gateway/modules/features/teamsbot/datamodelTeamsbot.py
patrick-motsch de573fd834 refactor: add TransferMode, remove backgroundImageUrl and botAccount fields from config
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-17 18:34:24 +01:00

263 lines
16 KiB
Python

# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""
Teamsbot Feature - Data Models.
Pydantic models for Teams Bot sessions, transcripts, bot responses, and configuration.
"""
from typing import Optional, List, Dict, Any
from pydantic import BaseModel, Field
from enum import Enum
import uuid
# ============================================================================
# Enums
# ============================================================================
class TeamsbotSessionStatus(str, Enum):
"""Status of a Teams Bot session."""
PENDING = "pending" # Session created, waiting for bridge
JOINING = "joining" # Bridge is joining the meeting
ACTIVE = "active" # Bot is in the meeting and processing audio
LEAVING = "leaving" # Bot is leaving the meeting
ENDED = "ended" # Session completed normally
ERROR = "error" # Session ended with an error
class TeamsbotResponseType(str, Enum):
"""Type of bot response delivery."""
AUDIO = "audio" # Voice response only
CHAT = "chat" # Chat message only
BOTH = "both" # Voice + chat message
class TeamsbotDetectedIntent(str, Enum):
"""Intent detected by the SPEECH_TEAMS AI handler."""
ADDRESSED = "addressed" # Bot was directly addressed
QUESTION = "question" # A general question was asked
PROACTIVE = "proactive" # Bot has a valuable proactive contribution
STOP = "stop" # User asked the bot to stop/be quiet
NONE = "none" # No action needed
class TeamsbotResponseMode(str, Enum):
"""How the bot should respond."""
AUTO = "auto" # Fully automatic: AI decides when to respond
MANUAL = "manual" # User triggers responses manually from UI
TRANSCRIBE_ONLY = "transcribeOnly" # Only transcribe, no AI responses
class TeamsbotResponseChannel(str, Enum):
"""Channel for bot responses."""
VOICE = "voice" # Bot responds only via voice (TTS)
CHAT = "chat" # Bot responds only via chat message
BOTH = "both" # Bot responds via voice AND chat
class TeamsbotJoinMode(str, Enum):
"""How the bot joins the meeting."""
SYSTEM_BOT = "systemBot" # Join with system bot account (backend-managed credentials)
ANONYMOUS = "anonymous" # Join as anonymous guest
USER_ACCOUNT = "userAccount" # Join with user's own Microsoft account (OAuth)
class TeamsbotTransferMode(str, Enum):
"""How meeting audio/transcript is transferred from bot to gateway."""
CAPTION = "caption" # Use Teams live captions (text scraping from DOM)
AUDIO = "audio" # Capture meeting audio and stream to gateway for STT
AUTO = "auto" # Automatic: anonymous → audio, authenticated → caption
# ============================================================================
# Database Models (stored in PostgreSQL)
# ============================================================================
class TeamsbotSession(BaseModel):
"""A Teams Bot meeting session."""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Session ID")
instanceId: str = Field(description="Feature instance ID (FK)")
mandateId: str = Field(description="Mandate ID (FK)")
meetingLink: str = Field(description="Teams meeting join link")
botName: str = Field(default="AI Assistant", description="Display name of the bot in the meeting")
status: TeamsbotSessionStatus = Field(default=TeamsbotSessionStatus.PENDING, description="Current session status")
startedAt: Optional[str] = Field(default=None, description="ISO timestamp when session started")
endedAt: Optional[str] = Field(default=None, description="ISO timestamp when session ended")
startedByUserId: str = Field(description="User ID who started the session")
bridgeSessionId: Optional[str] = Field(default=None, description="Session ID on the .NET Media Bridge")
meetingChatId: Optional[str] = Field(default=None, description="Teams meeting chat ID for Graph API messages")
sessionContext: Optional[str] = Field(default=None, description="Custom context/knowledge provided by the user for this session")
summary: Optional[str] = Field(default=None, description="AI-generated meeting summary (after session ends)")
errorMessage: Optional[str] = Field(default=None, description="Error message if status is ERROR")
transcriptSegmentCount: int = Field(default=0, description="Number of transcript segments in this session")
botResponseCount: int = Field(default=0, description="Number of bot responses in this session")
creationDate: Optional[str] = Field(default=None, description="ISO timestamp of record creation")
lastModified: Optional[str] = Field(default=None, description="ISO timestamp of last modification")
class TeamsbotTranscript(BaseModel):
"""A single transcript segment from the meeting."""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Transcript segment ID")
sessionId: str = Field(description="Session ID (FK)")
speaker: Optional[str] = Field(default=None, description="Speaker name or identifier")
text: str = Field(description="Transcribed text")
timestamp: str = Field(description="ISO timestamp of the speech segment")
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")
creationDate: Optional[str] = Field(default=None, description="ISO timestamp of record creation")
class TeamsbotBotResponse(BaseModel):
"""A bot response generated during a meeting session."""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Response ID")
sessionId: str = Field(description="Session ID (FK)")
responseText: str = Field(description="The bot's response text")
responseType: TeamsbotResponseType = Field(default=TeamsbotResponseType.BOTH, description="How the response was delivered")
detectedIntent: TeamsbotDetectedIntent = Field(default=TeamsbotDetectedIntent.NONE, description="What triggered the response")
reasoning: Optional[str] = Field(default=None, description="AI reasoning for why it responded")
triggeredByTranscriptId: Optional[str] = Field(default=None, description="Transcript segment that triggered this response")
modelName: Optional[str] = Field(default=None, description="AI model used for this response")
processingTime: float = Field(default=0.0, description="Processing time in seconds")
priceCHF: float = Field(default=0.0, description="Cost of this AI call in CHF")
timestamp: Optional[str] = Field(default=None, description="ISO timestamp of the response")
creationDate: Optional[str] = Field(default=None, description="ISO timestamp of record creation")
# ============================================================================
# System Bot Accounts (stored in PostgreSQL, credentials encrypted)
# ============================================================================
class TeamsbotSystemBot(BaseModel):
"""A system bot account for authenticated meeting joins.
Credentials are stored encrypted in the database, NOT in the UI-visible config.
Only mandate admins can manage system bots."""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="System bot ID")
mandateId: str = Field(description="Mandate ID (FK) - bots are scoped to mandates")
name: str = Field(description="Display name (e.g. 'Nyla Larsson')")
email: str = Field(description="Microsoft account email")
encryptedPassword: str = Field(description="Encrypted Microsoft account password")
isActive: bool = Field(default=True, description="Whether this bot account is active")
creationDate: Optional[str] = Field(default=None, description="ISO timestamp of creation")
lastModified: Optional[str] = Field(default=None, description="ISO timestamp of last modification")
# ============================================================================
# Per-User Settings (stored in PostgreSQL, per user per instance)
# ============================================================================
class TeamsbotUserSettings(BaseModel):
"""Per-user settings for the Teams Bot feature.
Each user has their own settings per feature instance.
These override the instance-level defaults (TeamsbotConfig)."""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Settings ID")
userId: str = Field(description="User ID (FK)")
instanceId: str = Field(description="Feature instance ID (FK)")
botName: Optional[str] = Field(default=None, description="Bot display name override")
aiSystemPrompt: Optional[str] = Field(default=None, description="AI system prompt override")
responseMode: Optional[str] = Field(default=None, description="Response mode override: auto, manual, transcribeOnly")
responseChannel: Optional[str] = Field(default=None, description="Response channel override: voice, chat, both")
transferMode: Optional[str] = Field(default=None, description="Transfer mode override: caption, audio, auto")
language: Optional[str] = Field(default=None, description="Language override (e.g. de-DE)")
voiceId: Optional[str] = Field(default=None, description="TTS voice ID override")
triggerIntervalSeconds: Optional[int] = Field(default=None, description="Trigger interval override")
triggerCooldownSeconds: Optional[int] = Field(default=None, description="Trigger cooldown override")
contextWindowSegments: Optional[int] = Field(default=None, description="Context window override")
creationDate: Optional[str] = Field(default=None, description="ISO timestamp of creation")
lastModified: Optional[str] = Field(default=None, description="ISO timestamp of last modification")
# ============================================================================
# Configuration Model (stored in FeatureInstance.config JSONB -- serves as DEFAULT template)
# ============================================================================
class TeamsbotConfig(BaseModel):
"""Configuration for a Teams Bot feature instance (serves as default template for new users)."""
botName: str = Field(default="AI Assistant", description="Default bot display name")
aiSystemPrompt: str = Field(
default="Du bist ein hilfreicher Meeting-Assistent. Fasse wichtige Punkte zusammen und beantworte Fragen sachlich.",
description="Custom system prompt for the AI analysis"
)
responseMode: TeamsbotResponseMode = Field(default=TeamsbotResponseMode.AUTO, description="How the bot responds")
responseChannel: TeamsbotResponseChannel = Field(default=TeamsbotResponseChannel.VOICE, description="Channel for bot responses: voice, chat, or both")
transferMode: TeamsbotTransferMode = Field(default=TeamsbotTransferMode.AUTO, description="How meeting content is captured: caption (Teams captions), audio (stream to gateway STT), or auto")
language: str = Field(default="de-DE", description="Primary language for STT/TTS")
voiceId: Optional[str] = Field(default=None, description="Google TTS voice ID (e.g., de-DE-Standard-A)")
browserBotUrl: Optional[str] = Field(default=None, description="URL of the Browser Bot service. Falls back to TEAMSBOT_BROWSER_BOT_URL env variable if not set per-instance.")
triggerIntervalSeconds: int = Field(default=10, ge=3, le=60, description="Seconds between periodic AI analysis triggers")
triggerCooldownSeconds: int = Field(default=3, ge=1, le=30, description="Minimum seconds between AI calls")
contextWindowSegments: int = Field(default=20, ge=5, le=100, description="Number of transcript segments to include in AI context")
def _getEffectiveBrowserBotUrl(self) -> Optional[str]:
"""Resolve the effective browser bot URL: per-instance config takes priority, then env variable."""
if self.browserBotUrl:
return self.browserBotUrl
from modules.shared.configuration import APP_CONFIG
return APP_CONFIG.get("TEAMSBOT_BROWSER_BOT_URL")
# ============================================================================
# API Request/Response Models
# ============================================================================
class TeamsbotStartSessionRequest(BaseModel):
"""Request to start a new Teams Bot session."""
meetingLink: str = Field(description="Teams meeting join link (e.g., https://teams.microsoft.com/l/meetup-join/...)")
botName: Optional[str] = Field(default=None, description="Override bot name for this session")
connectionId: Optional[str] = Field(default=None, description="Microsoft connection ID for Graph API access")
joinMode: Optional[TeamsbotJoinMode] = Field(default=None, description="How the bot joins: systemBot, anonymous, or userAccount. Defaults to systemBot if credentials configured, else anonymous.")
sessionContext: Optional[str] = Field(default=None, description="Custom context/knowledge to provide to the bot for this session (e.g. meeting agenda, documents, background info)")
class TeamsbotSessionResponse(BaseModel):
"""Response for session details."""
session: TeamsbotSession
transcripts: Optional[List[TeamsbotTranscript]] = Field(default=None, description="Transcript segments (if requested)")
botResponses: Optional[List[TeamsbotBotResponse]] = Field(default=None, description="Bot responses (if requested)")
class TeamsbotConfigUpdateRequest(BaseModel):
"""Request to update teamsbot configuration."""
botName: Optional[str] = None
aiSystemPrompt: Optional[str] = None
responseMode: Optional[TeamsbotResponseMode] = None
responseChannel: Optional[TeamsbotResponseChannel] = None
transferMode: Optional[TeamsbotTransferMode] = None
language: Optional[str] = None
voiceId: Optional[str] = None
browserBotUrl: Optional[str] = None
triggerIntervalSeconds: Optional[int] = None
triggerCooldownSeconds: Optional[int] = None
contextWindowSegments: Optional[int] = None
# ============================================================================
# SPEECH_TEAMS AI Response Model
# ============================================================================
class SpeechTeamsResponse(BaseModel):
"""Structured response from the SPEECH_TEAMS AI handler."""
shouldRespond: bool = Field(description="Whether the bot should respond")
responseText: Optional[str] = Field(default=None, description="The bot's response text (only if shouldRespond=True)")
reasoning: str = Field(default="", description="Reasoning for the decision (for logging/debug)")
detectedIntent: str = Field(default="none", description="Detected intent: addressed, question, proactive, none")
# ============================================================================
# Bridge Communication Models
# ============================================================================
class BridgeJoinRequest(BaseModel):
"""Request sent to .NET Media Bridge to join a meeting."""
meetingLink: str = Field(description="Teams meeting join link")
botName: str = Field(description="Bot display name")
gatewayCallbackUrl: str = Field(description="Gateway URL for bridge callbacks")
gatewayWsUrl: str = Field(description="Gateway WebSocket URL for audio streaming")
sessionId: str = Field(description="Session ID for correlation")
gatewayBaseUrl: str = Field(description="Base URL of this gateway instance (e.g. https://gateway-prod.poweron-center.net)")
class BridgeStatusResponse(BaseModel):
"""Status response from the .NET Media Bridge."""
healthy: bool = Field(description="Whether the bridge is healthy")
activeSessions: int = Field(default=0, description="Number of active meeting sessions")
sessions: Optional[List[Dict[str, Any]]] = Field(default=None, description="Details of active sessions")