196 lines
11 KiB
Python
196 lines
11 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)")
|
|
bridgeUrl: Optional[str] = Field(default=None, description="URL of the .NET Media Bridge service. Falls back to TEAMSBOT_BRIDGE_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 _getEffectiveBridgeUrl(self) -> Optional[str]:
|
|
"""Resolve the effective bridge URL: per-instance config takes priority, then env variable."""
|
|
if self.bridgeUrl:
|
|
return self.bridgeUrl
|
|
from modules.shared.configuration import APP_CONFIG
|
|
return APP_CONFIG.get("TEAMSBOT_BRIDGE_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
|
|
bridgeUrl: 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")
|