# 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")