gateway/modules/features/teamsbot/datamodelTeamsbot.py
patrick-motsch ad5c9d10cd feat(teamsbot): dedicated bot account support with authenticated join
- New config fields: botAccountEmail, botAccountPassword for dedicated MSFT account
- BrowserBotConnector passes credentials + backgroundImageUrl to bot service
- Service passes config credentials to connector in joinMeeting
- Enables: full language settings, virtual background, no lobby wait
- Fallback: anonymous join when no bot account configured

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-15 11:56:04 +01:00

200 lines
12 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
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
# ============================================================================
# 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")
backgroundImageUrl: Optional[str] = Field(default=None, description="Background image URL for the bot's video feed")
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")
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")
# ============================================================================
# Configuration Model (stored in FeatureInstance.config JSONB)
# ============================================================================
class TeamsbotConfig(BaseModel):
"""Configuration for a Teams Bot feature instance."""
botName: str = Field(default="AI Assistant", description="Default bot display name")
backgroundImageUrl: Optional[str] = Field(default=None, description="Default background image URL")
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")
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.")
botAccountEmail: Optional[str] = Field(default=None, description="Dedicated Microsoft account email for authenticated bot join. Leave empty for anonymous join.")
botAccountPassword: Optional[str] = Field(default=None, description="Dedicated Microsoft account password. MFA must be disabled for this account.")
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")
backgroundImageUrl: Optional[str] = Field(default=None, description="Override background image for this session")
connectionId: Optional[str] = Field(default=None, description="Microsoft connection ID for Graph API access")
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
backgroundImageUrl: Optional[str] = None
aiSystemPrompt: Optional[str] = None
responseMode: Optional[TeamsbotResponseMode] = None
language: Optional[str] = None
voiceId: Optional[str] = None
browserBotUrl: Optional[str] = None
botAccountEmail: Optional[str] = None
botAccountPassword: 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")
backgroundImageUrl: Optional[str] = Field(default=None, description="Background image URL")
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")