263 lines
16 KiB
Python
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)
|
|
|
|
|
|
# ============================================================================
|
|
# 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")
|
|
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")
|
|
backgroundImageUrl: Optional[str] = Field(default=None, description="Background image URL 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")
|
|
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")
|
|
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")
|
|
responseChannel: TeamsbotResponseChannel = Field(default=TeamsbotResponseChannel.VOICE, description="Channel for bot responses: voice, chat, or both")
|
|
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")
|
|
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
|
|
backgroundImageUrl: Optional[str] = None
|
|
aiSystemPrompt: Optional[str] = None
|
|
responseMode: Optional[TeamsbotResponseMode] = None
|
|
responseChannel: Optional[TeamsbotResponseChannel] = 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")
|