# Copyright (c) 2025 Patrick Motsch # All rights reserved. """ CommCoach Feature - Data Models. Pydantic models for training modules, sessions, messages, tasks, scores, and user profiles. """ from typing import Optional, List, Dict, Any from pydantic import BaseModel, Field from enum import Enum from modules.datamodels.datamodelBase import PowerOnModel import uuid # ============================================================================ # Enums # ============================================================================ class TrainingModuleStatus(str, Enum): ACTIVE = "active" PAUSED = "paused" ARCHIVED = "archived" COMPLETED = "completed" class TrainingModuleType(str, Enum): COACHING = "coaching" TRAINING = "training" EXAM = "exam" ELEARNING = "elearning" class CoachingSessionStatus(str, Enum): ACTIVE = "active" COMPLETED = "completed" CANCELLED = "cancelled" class CoachingMessageRole(str, Enum): USER = "user" ASSISTANT = "assistant" SYSTEM = "system" class CoachingMessageContentType(str, Enum): TEXT = "text" AUDIO_TRANSCRIPT = "audioTranscript" SYSTEM_NOTE = "systemNote" class CoachingTaskStatus(str, Enum): OPEN = "open" IN_PROGRESS = "inProgress" DONE = "done" SKIPPED = "skipped" class CoachingTaskPriority(str, Enum): LOW = "low" MEDIUM = "medium" HIGH = "high" class CoachingScoreTrend(str, Enum): IMPROVING = "improving" STABLE = "stable" DECLINING = "declining" # ============================================================================ # Database Models # ============================================================================ class TrainingModule(PowerOnModel): """A training module representing a topic the user is working on.""" id: str = Field(default_factory=lambda: str(uuid.uuid4())) userId: str = Field(description="Owner user ID (strict ownership)") mandateId: str = Field(description="Mandate ID") instanceId: str = Field(description="Feature instance ID") title: str = Field(description="Module title, e.g. 'Conflict with team lead'") description: Optional[str] = Field(default=None, description="Short description") moduleType: TrainingModuleType = Field(default=TrainingModuleType.COACHING) status: TrainingModuleStatus = Field(default=TrainingModuleStatus.ACTIVE) goals: Optional[str] = Field(default=None, description="Free-text goal description") insights: Optional[str] = Field(default=None, description="JSON array of AI insights [{id, text, sessionId, createdAt}]") metadata: Optional[str] = Field(default=None, description="JSON object with flexible metadata") personaId: Optional[str] = Field(default=None, description="Default persona for sessions") kpiTargets: Optional[str] = Field(default=None, description="JSON object with structured KPI targets") sessionCount: int = Field(default=0) taskCount: int = Field(default=0) lastSessionAt: Optional[float] = Field(default=None, json_schema_extra={"frontend_type": "timestamp"}) rollingOverview: Optional[str] = Field(default=None, description="AI summary of older sessions for long context history") rollingOverviewUpToSessionCount: Optional[int] = Field(default=None, description="Session count covered by rollingOverview") class CoachingSession(PowerOnModel): """A single coaching conversation session within a module.""" id: str = Field(default_factory=lambda: str(uuid.uuid4())) moduleId: str = Field(description="FK to TrainingModule") userId: str = Field(description="Owner user ID") mandateId: str = Field(description="Mandate ID") instanceId: str = Field(description="Feature instance ID") status: CoachingSessionStatus = Field(default=CoachingSessionStatus.ACTIVE) personaId: Optional[str] = Field(default=None, description="FK to CoachingPersona (Iteration 2)") summary: Optional[str] = Field(default=None, description="AI-generated session summary") coachNotes: Optional[str] = Field(default=None, description="JSON: AI internal notes for continuity") compressedHistorySummary: Optional[str] = Field(default=None, description="AI summary of older messages for long sessions") compressedHistoryUpToMessageCount: Optional[int] = Field(default=None, description="Message count covered by compressedHistorySummary") keyTopics: Optional[str] = Field(default=None, description="JSON array of key topics extracted at session complete") durationSeconds: int = Field(default=0) messageCount: int = Field(default=0) competenceScore: Optional[float] = Field(default=None, ge=0.0, le=100.0) emailSent: bool = Field(default=False) startedAt: Optional[float] = Field(default=None, json_schema_extra={"frontend_type": "timestamp"}) endedAt: Optional[float] = Field(default=None, json_schema_extra={"frontend_type": "timestamp"}) class CoachingMessage(PowerOnModel): """A single message in a coaching session.""" id: str = Field(default_factory=lambda: str(uuid.uuid4())) sessionId: str = Field(description="FK to CoachingSession") moduleId: str = Field(description="FK to TrainingModule") userId: str = Field(description="Owner user ID") role: CoachingMessageRole = Field(description="Message author role") content: str = Field(description="Message content (Markdown)") contentType: CoachingMessageContentType = Field(default=CoachingMessageContentType.TEXT) audioRef: Optional[str] = Field(default=None, description="Reference to audio file") metadata: Optional[str] = Field(default=None, description="JSON: token count, voice info, etc.") class CoachingTask(PowerOnModel): """A task/checklist item assigned within a training module.""" id: str = Field(default_factory=lambda: str(uuid.uuid4())) moduleId: str = Field(description="FK to TrainingModule") sessionId: Optional[str] = Field(default=None, description="FK to originating session") userId: str = Field(description="Owner user ID") mandateId: str = Field(description="Mandate ID") title: str = Field(description="Task title") description: Optional[str] = Field(default=None) status: CoachingTaskStatus = Field(default=CoachingTaskStatus.OPEN) priority: CoachingTaskPriority = Field(default=CoachingTaskPriority.MEDIUM) dueDate: Optional[float] = Field(default=None, json_schema_extra={"frontend_type": "date"}) completedAt: Optional[float] = Field(default=None, json_schema_extra={"frontend_type": "timestamp"}) class CoachingScore(PowerOnModel): """A competence score for a dimension, recorded after a session.""" id: str = Field(default_factory=lambda: str(uuid.uuid4())) moduleId: str = Field(description="FK to TrainingModule") sessionId: str = Field(description="FK to CoachingSession") userId: str = Field(description="Owner user ID") mandateId: str = Field(description="Mandate ID") dimension: str = Field(description="e.g. empathy, clarity, assertiveness, listening") score: float = Field(ge=0.0, le=100.0) trend: CoachingScoreTrend = Field(default=CoachingScoreTrend.STABLE) evidence: Optional[str] = Field(default=None, description="AI reasoning for the score") class CoachingUserProfile(PowerOnModel): """Per-user coaching profile and preferences.""" id: str = Field(default_factory=lambda: str(uuid.uuid4())) userId: str = Field(description="Owner user ID") mandateId: str = Field(description="Mandate ID") instanceId: str = Field(description="Feature instance ID") dailyReminderTime: Optional[str] = Field(default=None, description="HH:MM format") dailyReminderEnabled: bool = Field(default=False) emailSummaryEnabled: bool = Field(default=True) streakDays: int = Field(default=0) longestStreak: int = Field(default=0) totalSessions: int = Field(default=0) totalMinutes: int = Field(default=0) lastSessionAt: Optional[float] = Field(default=None, json_schema_extra={"frontend_type": "timestamp"}) # ============================================================================ # Iteration 2: Personas # ============================================================================ class CoachingPersona(PowerOnModel): """A roleplay persona for coaching sessions.""" id: str = Field(default_factory=lambda: str(uuid.uuid4())) userId: str = Field(description="Owner user ID ('system' for builtins)") mandateId: Optional[str] = Field(default=None) instanceId: Optional[str] = Field(default=None) key: str = Field(description="Unique key, e.g. 'critical_cfo_f'") label: str = Field(description="Display label, e.g. 'Kritische CFO'") description: str = Field(description="Detailed role description for the AI") systemPromptOverride: Optional[str] = Field(default=None, description="Full system prompt override for this persona") gender: Optional[str] = Field(default=None, description="m or f") category: str = Field(default="builtin", description="'builtin' or 'custom'") isActive: bool = Field(default=True) # ============================================================================ # Module-Persona Mapping (M:N) # ============================================================================ class ModulePersonaMapping(PowerOnModel): """Maps which personas are available for a specific training module.""" id: str = Field(default_factory=lambda: str(uuid.uuid4())) moduleId: str = Field(description="FK to TrainingModule") personaId: str = Field(description="FK to CoachingPersona") instanceId: str = Field(description="Feature instance ID") class SetModulePersonasRequest(BaseModel): personaIds: List[str] = Field(description="List of persona IDs to assign to this module") # ============================================================================ # Iteration 2: Badges / Gamification # ============================================================================ class CoachingBadge(PowerOnModel): """An achievement badge awarded to a user.""" id: str = Field(default_factory=lambda: str(uuid.uuid4())) userId: str = Field(description="Owner user ID") mandateId: str = Field(description="Mandate ID") instanceId: str = Field(description="Feature instance ID") badgeKey: str = Field(description="Badge identifier, e.g. 'streak_7'") awardedAt: Optional[float] = Field(default=None, json_schema_extra={"frontend_type": "timestamp"}) # ============================================================================ # API Request/Response Models # ============================================================================ class CreateModuleRequest(BaseModel): title: str = Field(description="Module title") description: Optional[str] = None moduleType: Optional[TrainingModuleType] = TrainingModuleType.COACHING goals: Optional[str] = None personaId: Optional[str] = None kpiTargets: Optional[str] = None class UpdateModuleRequest(BaseModel): title: Optional[str] = None description: Optional[str] = None moduleType: Optional[TrainingModuleType] = None goals: Optional[str] = None personaId: Optional[str] = None kpiTargets: Optional[str] = None class SendMessageRequest(BaseModel): content: str = Field(description="User message text") contentType: Optional[CoachingMessageContentType] = CoachingMessageContentType.TEXT fileIds: Optional[List[str]] = Field(default=None, description="Attached file IDs for agent context") dataSourceIds: Optional[List[str]] = Field(default=None, description="Personal data source IDs") featureDataSourceIds: Optional[List[str]] = Field(default=None, description="Feature data source IDs") allowedProviders: Optional[List[str]] = Field(default=None, description="Allowed AI providers") class CreateTaskRequest(BaseModel): title: str description: Optional[str] = None priority: Optional[CoachingTaskPriority] = CoachingTaskPriority.MEDIUM dueDate: Optional[float] = None class UpdateTaskRequest(BaseModel): title: Optional[str] = None description: Optional[str] = None priority: Optional[CoachingTaskPriority] = None dueDate: Optional[float] = None class UpdateTaskStatusRequest(BaseModel): status: CoachingTaskStatus class UpdateProfileRequest(BaseModel): dailyReminderTime: Optional[str] = None dailyReminderEnabled: Optional[bool] = None emailSummaryEnabled: Optional[bool] = None class StartSessionRequest(BaseModel): personaId: Optional[str] = None class CreatePersonaRequest(BaseModel): label: str description: str gender: Optional[str] = None systemPromptOverride: Optional[str] = None class UpdatePersonaRequest(BaseModel): label: Optional[str] = None description: Optional[str] = None gender: Optional[str] = None systemPromptOverride: Optional[str] = None isActive: Optional[bool] = None class DashboardData(BaseModel): """Aggregated dashboard data for the user.""" totalModules: int = 0 activeModules: int = 0 totalSessions: int = 0 totalMinutes: int = 0 streakDays: int = 0 longestStreak: int = 0 averageScore: Optional[float] = None recentScores: List[Dict[str, Any]] = Field(default_factory=list) openTasks: int = 0 completedTasks: int = 0 modules: List[Dict[str, Any]] = Field(default_factory=list)