feat(training): add Media Pipeline — TTS Audio, Presentation Video, Bulk Generation
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 48s
CI / test-python-backend-compliance (push) Successful in 35s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 20s
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 48s
CI / test-python-backend-compliance (push) Successful in 35s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 20s
Phase A: 8 new IT-Security training modules (SEC-PWD, SEC-DESK, SEC-KIAI, SEC-BYOD, SEC-VIDEO, SEC-USB, SEC-INC, SEC-HOME) with CTM entries. Bulk content and quiz generation endpoints for all 28 modules. Phase B: Piper TTS service (Python/FastAPI) for local German speech synthesis. training_media table, TTSClient in Go backend, audio generation endpoints, AudioPlayer component in frontend. MinIO storage integration. Phase C: FFmpeg presentation video pipeline — LLM generates slide scripts, ImageMagick renders 1920x1080 slides, FFmpeg combines with audio to MP4. VideoPlayer and ScriptPreview components in frontend. New files: 15 created, 9 modified - compliance-tts-service/ (Dockerfile, main.py, tts_engine.py, storage.py, slide_renderer.py, video_generator.py) - migrations 014-016 (training engine, IT-security modules, media table) - training package (models, store, content_generator, media, handlers) - frontend (AudioPlayer, VideoPlayer, ScriptPreview, api, types, page) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
183
ai-compliance-sdk/internal/training/assignment.go
Normal file
183
ai-compliance-sdk/internal/training/assignment.go
Normal file
@@ -0,0 +1,183 @@
|
||||
package training
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// timeNow is a package-level function for testing
|
||||
var timeNow = time.Now
|
||||
|
||||
// ComputeAssignments calculates all necessary assignments for a user based on
|
||||
// their roles, existing assignments, and deadlines. Returns new assignments
|
||||
// that need to be created.
|
||||
func ComputeAssignments(ctx context.Context, store *Store, tenantID uuid.UUID,
|
||||
userID uuid.UUID, userName, userEmail string, roleCodes []string, trigger string) ([]TrainingAssignment, error) {
|
||||
|
||||
if trigger == "" {
|
||||
trigger = string(TriggerManual)
|
||||
}
|
||||
|
||||
// Get all required modules for the user's roles
|
||||
requiredModules, err := ComputeRequiredModules(ctx, store, tenantID, roleCodes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get existing active assignments for this user
|
||||
existingAssignments, _, err := store.ListAssignments(ctx, tenantID, &AssignmentFilters{
|
||||
UserID: &userID,
|
||||
Limit: 1000,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Build a map of existing assignments by module_id for quick lookup
|
||||
existingByModule := make(map[uuid.UUID]*TrainingAssignment)
|
||||
for i := range existingAssignments {
|
||||
a := &existingAssignments[i]
|
||||
// Only consider non-expired, non-completed-and-expired assignments
|
||||
if a.Status != AssignmentStatusExpired {
|
||||
existingByModule[a.ModuleID] = a
|
||||
}
|
||||
}
|
||||
|
||||
var newAssignments []TrainingAssignment
|
||||
now := timeNow().UTC()
|
||||
|
||||
for _, module := range requiredModules {
|
||||
existing, hasExisting := existingByModule[module.ID]
|
||||
|
||||
// Skip if there's an active, valid assignment
|
||||
if hasExisting {
|
||||
switch existing.Status {
|
||||
case AssignmentStatusCompleted:
|
||||
// Check if the completed assignment is still valid
|
||||
if existing.CompletedAt != nil {
|
||||
validUntil := existing.CompletedAt.AddDate(0, 0, module.ValidityDays)
|
||||
if validUntil.After(now) {
|
||||
continue // Still valid, skip
|
||||
}
|
||||
}
|
||||
case AssignmentStatusPending, AssignmentStatusInProgress:
|
||||
continue // Assignment exists and is active
|
||||
case AssignmentStatusOverdue:
|
||||
continue // Already tracked as overdue
|
||||
}
|
||||
}
|
||||
|
||||
// Determine the role code for this assignment
|
||||
roleCode := ""
|
||||
for _, role := range roleCodes {
|
||||
entries, err := store.GetMatrixForRole(ctx, tenantID, role)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if entry.ModuleID == module.ID {
|
||||
roleCode = role
|
||||
break
|
||||
}
|
||||
}
|
||||
if roleCode != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate deadline based on frequency
|
||||
var deadline time.Time
|
||||
switch module.FrequencyType {
|
||||
case FrequencyOnboarding:
|
||||
deadline = now.AddDate(0, 0, 30) // 30 days for onboarding
|
||||
case FrequencyMicro:
|
||||
deadline = now.AddDate(0, 0, 14) // 14 days for micro
|
||||
default:
|
||||
deadline = now.AddDate(0, 0, 90) // 90 days default
|
||||
}
|
||||
|
||||
assignment := TrainingAssignment{
|
||||
TenantID: tenantID,
|
||||
ModuleID: module.ID,
|
||||
UserID: userID,
|
||||
UserName: userName,
|
||||
UserEmail: userEmail,
|
||||
RoleCode: roleCode,
|
||||
TriggerType: TriggerType(trigger),
|
||||
Status: AssignmentStatusPending,
|
||||
Deadline: deadline,
|
||||
ModuleCode: module.ModuleCode,
|
||||
ModuleTitle: module.Title,
|
||||
}
|
||||
|
||||
// Create the assignment in the store
|
||||
if err := store.CreateAssignment(ctx, &assignment); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Log the assignment
|
||||
store.LogAction(ctx, &AuditLogEntry{
|
||||
TenantID: tenantID,
|
||||
UserID: &userID,
|
||||
Action: AuditActionAssigned,
|
||||
EntityType: AuditEntityAssignment,
|
||||
EntityID: &assignment.ID,
|
||||
Details: map[string]interface{}{
|
||||
"module_code": module.ModuleCode,
|
||||
"trigger": trigger,
|
||||
"role_code": roleCode,
|
||||
"deadline": deadline.Format(time.RFC3339),
|
||||
},
|
||||
})
|
||||
|
||||
newAssignments = append(newAssignments, assignment)
|
||||
}
|
||||
|
||||
if newAssignments == nil {
|
||||
newAssignments = []TrainingAssignment{}
|
||||
}
|
||||
|
||||
return newAssignments, nil
|
||||
}
|
||||
|
||||
// BulkAssign assigns a module to all users with specific roles
|
||||
// Returns the number of assignments created
|
||||
func BulkAssign(ctx context.Context, store *Store, tenantID uuid.UUID,
|
||||
moduleID uuid.UUID, users []UserInfo, trigger string, deadline time.Time) (int, error) {
|
||||
|
||||
if trigger == "" {
|
||||
trigger = string(TriggerManual)
|
||||
}
|
||||
|
||||
count := 0
|
||||
for _, user := range users {
|
||||
assignment := TrainingAssignment{
|
||||
TenantID: tenantID,
|
||||
ModuleID: moduleID,
|
||||
UserID: user.UserID,
|
||||
UserName: user.UserName,
|
||||
UserEmail: user.UserEmail,
|
||||
RoleCode: user.RoleCode,
|
||||
TriggerType: TriggerType(trigger),
|
||||
Status: AssignmentStatusPending,
|
||||
Deadline: deadline,
|
||||
}
|
||||
|
||||
if err := store.CreateAssignment(ctx, &assignment); err != nil {
|
||||
return count, err
|
||||
}
|
||||
count++
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// UserInfo contains basic user information for bulk operations
|
||||
type UserInfo struct {
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
UserName string `json:"user_name"`
|
||||
UserEmail string `json:"user_email"`
|
||||
RoleCode string `json:"role_code"`
|
||||
}
|
||||
602
ai-compliance-sdk/internal/training/content_generator.go
Normal file
602
ai-compliance-sdk/internal/training/content_generator.go
Normal file
@@ -0,0 +1,602 @@
|
||||
package training
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/llm"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// ContentGenerator generates training content and quiz questions via LLM
|
||||
type ContentGenerator struct {
|
||||
registry *llm.ProviderRegistry
|
||||
piiDetector *llm.PIIDetector
|
||||
store *Store
|
||||
ttsClient *TTSClient
|
||||
}
|
||||
|
||||
// NewContentGenerator creates a new content generator
|
||||
func NewContentGenerator(registry *llm.ProviderRegistry, piiDetector *llm.PIIDetector, store *Store, ttsClient *TTSClient) *ContentGenerator {
|
||||
return &ContentGenerator{
|
||||
registry: registry,
|
||||
piiDetector: piiDetector,
|
||||
store: store,
|
||||
ttsClient: ttsClient,
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateModuleContent generates training content for a module via LLM
|
||||
func (g *ContentGenerator) GenerateModuleContent(ctx context.Context, module TrainingModule, language string) (*ModuleContent, error) {
|
||||
if language == "" {
|
||||
language = "de"
|
||||
}
|
||||
|
||||
prompt := buildContentPrompt(module, language)
|
||||
|
||||
resp, err := g.registry.Chat(ctx, &llm.ChatRequest{
|
||||
Messages: []llm.Message{
|
||||
{Role: "system", Content: getContentSystemPrompt(language)},
|
||||
{Role: "user", Content: prompt},
|
||||
},
|
||||
Temperature: 0.15,
|
||||
MaxTokens: 4096,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("LLM content generation failed: %w", err)
|
||||
}
|
||||
|
||||
contentBody := resp.Message.Content
|
||||
|
||||
// PII check on generated content
|
||||
if g.piiDetector != nil && g.piiDetector.ContainsPII(contentBody) {
|
||||
findings := g.piiDetector.FindPII(contentBody)
|
||||
for _, f := range findings {
|
||||
contentBody = strings.ReplaceAll(contentBody, f.Match, "[REDACTED]")
|
||||
}
|
||||
}
|
||||
|
||||
// Create summary (first 200 chars)
|
||||
summary := contentBody
|
||||
if len(summary) > 200 {
|
||||
summary = summary[:200] + "..."
|
||||
}
|
||||
|
||||
content := &ModuleContent{
|
||||
ModuleID: module.ID,
|
||||
ContentFormat: ContentFormatMarkdown,
|
||||
ContentBody: contentBody,
|
||||
Summary: summary,
|
||||
GeneratedBy: "llm_" + resp.Provider,
|
||||
LLMModel: resp.Model,
|
||||
IsPublished: false,
|
||||
}
|
||||
|
||||
if err := g.store.CreateModuleContent(ctx, content); err != nil {
|
||||
return nil, fmt.Errorf("failed to save content: %w", err)
|
||||
}
|
||||
|
||||
// Audit log
|
||||
g.store.LogAction(ctx, &AuditLogEntry{
|
||||
TenantID: module.TenantID,
|
||||
Action: AuditActionContentGenerated,
|
||||
EntityType: AuditEntityModule,
|
||||
EntityID: &module.ID,
|
||||
Details: map[string]interface{}{
|
||||
"module_code": module.ModuleCode,
|
||||
"provider": resp.Provider,
|
||||
"model": resp.Model,
|
||||
"content_id": content.ID.String(),
|
||||
"version": content.Version,
|
||||
"tokens_used": resp.Usage.TotalTokens,
|
||||
},
|
||||
})
|
||||
|
||||
return content, nil
|
||||
}
|
||||
|
||||
// GenerateQuizQuestions generates quiz questions for a module based on its content
|
||||
func (g *ContentGenerator) GenerateQuizQuestions(ctx context.Context, module TrainingModule, count int) ([]QuizQuestion, error) {
|
||||
if count <= 0 {
|
||||
count = 5
|
||||
}
|
||||
|
||||
// Get the published content for context
|
||||
content, err := g.store.GetPublishedContent(ctx, module.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
contentContext := ""
|
||||
if content != nil {
|
||||
contentContext = content.ContentBody
|
||||
}
|
||||
|
||||
prompt := buildQuizPrompt(module, contentContext, count)
|
||||
|
||||
resp, err := g.registry.Chat(ctx, &llm.ChatRequest{
|
||||
Messages: []llm.Message{
|
||||
{Role: "system", Content: getQuizSystemPrompt()},
|
||||
{Role: "user", Content: prompt},
|
||||
},
|
||||
Temperature: 0.2,
|
||||
MaxTokens: 4096,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("LLM quiz generation failed: %w", err)
|
||||
}
|
||||
|
||||
// Parse the JSON response
|
||||
questions, err := parseQuizResponse(resp.Message.Content, module.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse quiz response: %w", err)
|
||||
}
|
||||
|
||||
// Save questions to store
|
||||
for i := range questions {
|
||||
questions[i].SortOrder = i + 1
|
||||
if err := g.store.CreateQuizQuestion(ctx, &questions[i]); err != nil {
|
||||
return nil, fmt.Errorf("failed to save question %d: %w", i+1, err)
|
||||
}
|
||||
}
|
||||
|
||||
return questions, nil
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Prompt Templates
|
||||
// ============================================================================
|
||||
|
||||
func getContentSystemPrompt(language string) string {
|
||||
if language == "en" {
|
||||
return "You are a compliance training content expert. Generate professional, accurate training material in Markdown format. Focus on practical relevance and legal accuracy. Do not include any personal data or fictional names."
|
||||
}
|
||||
return "Du bist ein Experte fuer Compliance-Schulungsinhalte. Erstelle professionelle, praezise Schulungsmaterialien im Markdown-Format. Fokussiere dich auf praktische Relevanz und rechtliche Genauigkeit. Verwende keine personenbezogenen Daten oder fiktiven Namen."
|
||||
}
|
||||
|
||||
func getQuizSystemPrompt() string {
|
||||
return `Du bist ein Experte fuer Compliance-Pruefungsfragen. Erstelle Multiple-Choice-Fragen als JSON-Array.
|
||||
Jede Frage hat genau 4 Antwortoptionen, davon genau eine richtige.
|
||||
Antworte NUR mit dem JSON-Array, ohne zusaetzlichen Text.
|
||||
|
||||
Format:
|
||||
[
|
||||
{
|
||||
"question": "Frage hier?",
|
||||
"options": ["Option A", "Option B", "Option C", "Option D"],
|
||||
"correct_index": 0,
|
||||
"explanation": "Erklaerung warum Option A richtig ist.",
|
||||
"difficulty": "medium"
|
||||
}
|
||||
]`
|
||||
}
|
||||
|
||||
func buildContentPrompt(module TrainingModule, language string) string {
|
||||
regulationLabels := map[RegulationArea]string{
|
||||
RegulationDSGVO: "Datenschutz-Grundverordnung (DSGVO)",
|
||||
RegulationNIS2: "NIS-2-Richtlinie",
|
||||
RegulationISO27001: "ISO 27001 / ISMS",
|
||||
RegulationAIAct: "EU AI Act / KI-Verordnung",
|
||||
RegulationGeschGehG: "Geschaeftsgeheimnisgesetz (GeschGehG)",
|
||||
RegulationHinSchG: "Hinweisgeberschutzgesetz (HinSchG)",
|
||||
}
|
||||
|
||||
regulation := regulationLabels[module.RegulationArea]
|
||||
if regulation == "" {
|
||||
regulation = string(module.RegulationArea)
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`Erstelle Schulungsmaterial fuer folgendes Compliance-Modul:
|
||||
|
||||
**Modulcode:** %s
|
||||
**Titel:** %s
|
||||
**Beschreibung:** %s
|
||||
**Regulierungsbereich:** %s
|
||||
**Dauer:** %d Minuten
|
||||
**NIS2-relevant:** %v
|
||||
|
||||
Das Material soll:
|
||||
1. Eine kurze Einfuehrung in das Thema geben
|
||||
2. Die wichtigsten rechtlichen Grundlagen erklaeren
|
||||
3. Praktische Handlungsanweisungen fuer den Arbeitsalltag enthalten
|
||||
4. Typische Fehler und Risiken aufzeigen
|
||||
5. Eine Zusammenfassung der Kernpunkte bieten
|
||||
|
||||
Verwende klare, verstaendliche Sprache. Zielgruppe sind Mitarbeiter in Unternehmen (50-1.500 MA).
|
||||
Formatiere den Inhalt als Markdown mit Ueberschriften, Aufzaehlungen und Hervorhebungen.`,
|
||||
module.ModuleCode, module.Title, module.Description,
|
||||
regulation, module.DurationMinutes, module.NIS2Relevant)
|
||||
}
|
||||
|
||||
func buildQuizPrompt(module TrainingModule, contentContext string, count int) string {
|
||||
prompt := fmt.Sprintf(`Erstelle %d Multiple-Choice-Pruefungsfragen fuer das Compliance-Modul:
|
||||
|
||||
**Modulcode:** %s
|
||||
**Titel:** %s
|
||||
**Regulierungsbereich:** %s`, count, module.ModuleCode, module.Title, string(module.RegulationArea))
|
||||
|
||||
if contentContext != "" {
|
||||
// Truncate content to avoid token limit
|
||||
if len(contentContext) > 3000 {
|
||||
contentContext = contentContext[:3000] + "..."
|
||||
}
|
||||
prompt += fmt.Sprintf(`
|
||||
|
||||
**Schulungsinhalt als Kontext:**
|
||||
%s`, contentContext)
|
||||
}
|
||||
|
||||
prompt += fmt.Sprintf(`
|
||||
|
||||
Erstelle genau %d Fragen mit je 4 Antwortoptionen.
|
||||
Verteile die Schwierigkeitsgrade: easy, medium, hard.
|
||||
Antworte NUR mit dem JSON-Array.`, count)
|
||||
|
||||
return prompt
|
||||
}
|
||||
|
||||
// parseQuizResponse parses LLM JSON response into QuizQuestion structs
|
||||
func parseQuizResponse(response string, moduleID uuid.UUID) ([]QuizQuestion, error) {
|
||||
// Try to extract JSON from the response (LLM might add text around it)
|
||||
jsonStr := response
|
||||
start := strings.Index(response, "[")
|
||||
end := strings.LastIndex(response, "]")
|
||||
if start >= 0 && end > start {
|
||||
jsonStr = response[start : end+1]
|
||||
}
|
||||
|
||||
type rawQuestion struct {
|
||||
Question string `json:"question"`
|
||||
Options []string `json:"options"`
|
||||
CorrectIndex int `json:"correct_index"`
|
||||
Explanation string `json:"explanation"`
|
||||
Difficulty string `json:"difficulty"`
|
||||
}
|
||||
|
||||
var rawQuestions []rawQuestion
|
||||
if err := json.Unmarshal([]byte(jsonStr), &rawQuestions); err != nil {
|
||||
return nil, fmt.Errorf("invalid JSON from LLM: %w", err)
|
||||
}
|
||||
|
||||
var questions []QuizQuestion
|
||||
for _, rq := range rawQuestions {
|
||||
difficulty := Difficulty(rq.Difficulty)
|
||||
if difficulty != DifficultyEasy && difficulty != DifficultyMedium && difficulty != DifficultyHard {
|
||||
difficulty = DifficultyMedium
|
||||
}
|
||||
|
||||
q := QuizQuestion{
|
||||
ModuleID: moduleID,
|
||||
Question: rq.Question,
|
||||
Options: rq.Options,
|
||||
CorrectIndex: rq.CorrectIndex,
|
||||
Explanation: rq.Explanation,
|
||||
Difficulty: difficulty,
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
if len(q.Options) != 4 {
|
||||
continue // Skip malformed questions
|
||||
}
|
||||
if q.CorrectIndex < 0 || q.CorrectIndex >= len(q.Options) {
|
||||
continue
|
||||
}
|
||||
|
||||
questions = append(questions, q)
|
||||
}
|
||||
|
||||
if questions == nil {
|
||||
questions = []QuizQuestion{}
|
||||
}
|
||||
|
||||
return questions, nil
|
||||
}
|
||||
|
||||
// GenerateAllModuleContent generates text content for all modules that don't have published content yet
|
||||
func (g *ContentGenerator) GenerateAllModuleContent(ctx context.Context, tenantID uuid.UUID, language string) (*BulkResult, error) {
|
||||
if language == "" {
|
||||
language = "de"
|
||||
}
|
||||
|
||||
modules, _, err := g.store.ListModules(ctx, tenantID, &ModuleFilters{Limit: 100})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list modules: %w", err)
|
||||
}
|
||||
|
||||
result := &BulkResult{}
|
||||
for _, module := range modules {
|
||||
// Check if module already has published content
|
||||
content, _ := g.store.GetPublishedContent(ctx, module.ID)
|
||||
if content != nil {
|
||||
result.Skipped++
|
||||
continue
|
||||
}
|
||||
|
||||
_, err := g.GenerateModuleContent(ctx, module, language)
|
||||
if err != nil {
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("%s: %v", module.ModuleCode, err))
|
||||
continue
|
||||
}
|
||||
result.Generated++
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GenerateAllQuizQuestions generates quiz questions for all modules that don't have questions yet
|
||||
func (g *ContentGenerator) GenerateAllQuizQuestions(ctx context.Context, tenantID uuid.UUID, count int) (*BulkResult, error) {
|
||||
if count <= 0 {
|
||||
count = 5
|
||||
}
|
||||
|
||||
modules, _, err := g.store.ListModules(ctx, tenantID, &ModuleFilters{Limit: 100})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list modules: %w", err)
|
||||
}
|
||||
|
||||
result := &BulkResult{}
|
||||
for _, module := range modules {
|
||||
// Check if module already has quiz questions
|
||||
questions, _ := g.store.ListQuizQuestions(ctx, module.ID)
|
||||
if len(questions) > 0 {
|
||||
result.Skipped++
|
||||
continue
|
||||
}
|
||||
|
||||
_, err := g.GenerateQuizQuestions(ctx, module, count)
|
||||
if err != nil {
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("%s: %v", module.ModuleCode, err))
|
||||
continue
|
||||
}
|
||||
result.Generated++
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GenerateAudio generates audio for a module using the TTS service
|
||||
func (g *ContentGenerator) GenerateAudio(ctx context.Context, module TrainingModule) (*TrainingMedia, error) {
|
||||
// Get published content
|
||||
content, err := g.store.GetPublishedContent(ctx, module.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get content: %w", err)
|
||||
}
|
||||
if content == nil {
|
||||
return nil, fmt.Errorf("no published content for module %s", module.ModuleCode)
|
||||
}
|
||||
|
||||
if g.ttsClient == nil {
|
||||
return nil, fmt.Errorf("TTS client not configured")
|
||||
}
|
||||
|
||||
// Create media record (processing)
|
||||
media := &TrainingMedia{
|
||||
ModuleID: module.ID,
|
||||
ContentID: &content.ID,
|
||||
MediaType: MediaTypeAudio,
|
||||
Status: MediaStatusProcessing,
|
||||
Bucket: "compliance-training-audio",
|
||||
ObjectKey: fmt.Sprintf("audio/%s/%s.mp3", module.ID.String(), content.ID.String()),
|
||||
MimeType: "audio/mpeg",
|
||||
VoiceModel: "de_DE-thorsten-high",
|
||||
Language: "de",
|
||||
GeneratedBy: "tts_piper",
|
||||
}
|
||||
|
||||
if err := g.store.CreateMedia(ctx, media); err != nil {
|
||||
return nil, fmt.Errorf("failed to create media record: %w", err)
|
||||
}
|
||||
|
||||
// Call TTS service
|
||||
ttsResp, err := g.ttsClient.Synthesize(ctx, &TTSSynthesizeRequest{
|
||||
Text: content.ContentBody,
|
||||
Language: "de",
|
||||
Voice: "thorsten-high",
|
||||
ModuleID: module.ID.String(),
|
||||
ContentID: content.ID.String(),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
g.store.UpdateMediaStatus(ctx, media.ID, MediaStatusFailed, 0, 0, err.Error())
|
||||
return nil, fmt.Errorf("TTS synthesis failed: %w", err)
|
||||
}
|
||||
|
||||
// Update media record
|
||||
media.Status = MediaStatusCompleted
|
||||
media.FileSizeBytes = ttsResp.SizeBytes
|
||||
media.DurationSeconds = ttsResp.DurationSeconds
|
||||
media.ObjectKey = ttsResp.ObjectKey
|
||||
media.Bucket = ttsResp.Bucket
|
||||
|
||||
g.store.UpdateMediaStatus(ctx, media.ID, MediaStatusCompleted, ttsResp.SizeBytes, ttsResp.DurationSeconds, "")
|
||||
|
||||
// Audit log
|
||||
g.store.LogAction(ctx, &AuditLogEntry{
|
||||
TenantID: module.TenantID,
|
||||
Action: AuditAction("audio_generated"),
|
||||
EntityType: AuditEntityModule,
|
||||
EntityID: &module.ID,
|
||||
Details: map[string]interface{}{
|
||||
"module_code": module.ModuleCode,
|
||||
"media_id": media.ID.String(),
|
||||
"duration_seconds": ttsResp.DurationSeconds,
|
||||
"size_bytes": ttsResp.SizeBytes,
|
||||
},
|
||||
})
|
||||
|
||||
return media, nil
|
||||
}
|
||||
|
||||
// VideoScript represents a structured presentation script
|
||||
type VideoScript struct {
|
||||
Title string `json:"title"`
|
||||
Sections []VideoScriptSection `json:"sections"`
|
||||
}
|
||||
|
||||
// VideoScriptSection is one slide in the presentation
|
||||
type VideoScriptSection struct {
|
||||
Heading string `json:"heading"`
|
||||
Text string `json:"text"`
|
||||
BulletPoints []string `json:"bullet_points"`
|
||||
}
|
||||
|
||||
// GenerateVideoScript generates a structured video script from module content via LLM
|
||||
func (g *ContentGenerator) GenerateVideoScript(ctx context.Context, module TrainingModule) (*VideoScript, error) {
|
||||
content, err := g.store.GetPublishedContent(ctx, module.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get content: %w", err)
|
||||
}
|
||||
if content == nil {
|
||||
return nil, fmt.Errorf("no published content for module %s", module.ModuleCode)
|
||||
}
|
||||
|
||||
prompt := fmt.Sprintf(`Erstelle ein strukturiertes Folien-Script fuer eine Praesentations-Video-Schulung.
|
||||
|
||||
**Modul:** %s — %s
|
||||
**Inhalt:**
|
||||
%s
|
||||
|
||||
Erstelle 5-8 Folien. Jede Folie hat:
|
||||
- heading: Kurze Ueberschrift (max 60 Zeichen)
|
||||
- text: Erklaerungstext (1-2 Saetze)
|
||||
- bullet_points: 2-4 Kernpunkte
|
||||
|
||||
Antworte NUR mit einem JSON-Objekt in diesem Format:
|
||||
{
|
||||
"title": "Titel der Praesentation",
|
||||
"sections": [
|
||||
{
|
||||
"heading": "Folienueberschrift",
|
||||
"text": "Erklaerungstext fuer diese Folie.",
|
||||
"bullet_points": ["Punkt 1", "Punkt 2", "Punkt 3"]
|
||||
}
|
||||
]
|
||||
}`, module.ModuleCode, module.Title, truncateText(content.ContentBody, 3000))
|
||||
|
||||
resp, err := g.registry.Chat(ctx, &llm.ChatRequest{
|
||||
Messages: []llm.Message{
|
||||
{Role: "system", Content: "Du bist ein Experte fuer Compliance-Schulungspraesentationen. Erstelle strukturierte Folien-Scripts als JSON. Antworte NUR mit dem JSON-Objekt."},
|
||||
{Role: "user", Content: prompt},
|
||||
},
|
||||
Temperature: 0.15,
|
||||
MaxTokens: 4096,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("LLM video script generation failed: %w", err)
|
||||
}
|
||||
|
||||
// Parse JSON response
|
||||
var script VideoScript
|
||||
jsonStr := resp.Message.Content
|
||||
start := strings.Index(jsonStr, "{")
|
||||
end := strings.LastIndex(jsonStr, "}")
|
||||
if start >= 0 && end > start {
|
||||
jsonStr = jsonStr[start : end+1]
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(jsonStr), &script); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse video script JSON: %w", err)
|
||||
}
|
||||
|
||||
if len(script.Sections) == 0 {
|
||||
return nil, fmt.Errorf("video script has no sections")
|
||||
}
|
||||
|
||||
return &script, nil
|
||||
}
|
||||
|
||||
// GenerateVideo generates a presentation video for a module
|
||||
func (g *ContentGenerator) GenerateVideo(ctx context.Context, module TrainingModule) (*TrainingMedia, error) {
|
||||
if g.ttsClient == nil {
|
||||
return nil, fmt.Errorf("TTS client not configured")
|
||||
}
|
||||
|
||||
// Check for published audio, generate if missing
|
||||
audio, _ := g.store.GetPublishedAudio(ctx, module.ID)
|
||||
if audio == nil {
|
||||
// Try to generate audio first
|
||||
var err error
|
||||
audio, err = g.GenerateAudio(ctx, module)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("audio generation required but failed: %w", err)
|
||||
}
|
||||
// Auto-publish the audio
|
||||
g.store.PublishMedia(ctx, audio.ID, true)
|
||||
}
|
||||
|
||||
// Generate video script via LLM
|
||||
script, err := g.GenerateVideoScript(ctx, module)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("video script generation failed: %w", err)
|
||||
}
|
||||
|
||||
// Create media record
|
||||
media := &TrainingMedia{
|
||||
ModuleID: module.ID,
|
||||
MediaType: MediaTypeVideo,
|
||||
Status: MediaStatusProcessing,
|
||||
Bucket: "compliance-training-video",
|
||||
ObjectKey: fmt.Sprintf("video/%s/presentation.mp4", module.ID.String()),
|
||||
MimeType: "video/mp4",
|
||||
Language: "de",
|
||||
GeneratedBy: "tts_ffmpeg",
|
||||
}
|
||||
|
||||
if err := g.store.CreateMedia(ctx, media); err != nil {
|
||||
return nil, fmt.Errorf("failed to create media record: %w", err)
|
||||
}
|
||||
|
||||
// Build script map for TTS service
|
||||
scriptMap := map[string]interface{}{
|
||||
"title": script.Title,
|
||||
"module_code": module.ModuleCode,
|
||||
"sections": script.Sections,
|
||||
}
|
||||
|
||||
// Call TTS service video generation
|
||||
videoResp, err := g.ttsClient.GenerateVideo(ctx, &TTSGenerateVideoRequest{
|
||||
Script: scriptMap,
|
||||
AudioObjectKey: audio.ObjectKey,
|
||||
ModuleID: module.ID.String(),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
g.store.UpdateMediaStatus(ctx, media.ID, MediaStatusFailed, 0, 0, err.Error())
|
||||
return nil, fmt.Errorf("video generation failed: %w", err)
|
||||
}
|
||||
|
||||
// Update media record
|
||||
media.Status = MediaStatusCompleted
|
||||
media.FileSizeBytes = videoResp.SizeBytes
|
||||
media.DurationSeconds = videoResp.DurationSeconds
|
||||
media.ObjectKey = videoResp.ObjectKey
|
||||
media.Bucket = videoResp.Bucket
|
||||
|
||||
g.store.UpdateMediaStatus(ctx, media.ID, MediaStatusCompleted, videoResp.SizeBytes, videoResp.DurationSeconds, "")
|
||||
|
||||
// Audit log
|
||||
g.store.LogAction(ctx, &AuditLogEntry{
|
||||
TenantID: module.TenantID,
|
||||
Action: AuditAction("video_generated"),
|
||||
EntityType: AuditEntityModule,
|
||||
EntityID: &module.ID,
|
||||
Details: map[string]interface{}{
|
||||
"module_code": module.ModuleCode,
|
||||
"media_id": media.ID.String(),
|
||||
"duration_seconds": videoResp.DurationSeconds,
|
||||
"size_bytes": videoResp.SizeBytes,
|
||||
"slides": len(script.Sections),
|
||||
},
|
||||
})
|
||||
|
||||
return media, nil
|
||||
}
|
||||
|
||||
func truncateText(text string, maxLen int) string {
|
||||
if len(text) <= maxLen {
|
||||
return text
|
||||
}
|
||||
return text[:maxLen] + "..."
|
||||
}
|
||||
177
ai-compliance-sdk/internal/training/escalation.go
Normal file
177
ai-compliance-sdk/internal/training/escalation.go
Normal file
@@ -0,0 +1,177 @@
|
||||
package training
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// Escalation level thresholds (days overdue)
|
||||
const (
|
||||
EscalationThresholdL1 = 7 // Reminder to user
|
||||
EscalationThresholdL2 = 14 // Notify team lead
|
||||
EscalationThresholdL3 = 30 // Notify management
|
||||
EscalationThresholdL4 = 45 // Notify compliance officer
|
||||
)
|
||||
|
||||
// EscalationLabels maps levels to human-readable labels
|
||||
var EscalationLabels = map[int]string{
|
||||
0: "Keine Eskalation",
|
||||
1: "Erinnerung an Mitarbeiter",
|
||||
2: "Benachrichtigung Teamleitung",
|
||||
3: "Benachrichtigung Management",
|
||||
4: "Benachrichtigung Compliance Officer",
|
||||
}
|
||||
|
||||
// CheckEscalations checks all overdue assignments and escalates as needed
|
||||
func CheckEscalations(ctx context.Context, store *Store, tenantID uuid.UUID) ([]EscalationResult, error) {
|
||||
overdueAssignments, err := store.ListOverdueAssignments(ctx, tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var results []EscalationResult
|
||||
now := timeNow().UTC()
|
||||
|
||||
for _, assignment := range overdueAssignments {
|
||||
daysOverdue := int(math.Floor(now.Sub(assignment.Deadline).Hours() / 24))
|
||||
if daysOverdue < 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Determine new escalation level
|
||||
newLevel := 0
|
||||
if daysOverdue >= EscalationThresholdL4 {
|
||||
newLevel = 4
|
||||
} else if daysOverdue >= EscalationThresholdL3 {
|
||||
newLevel = 3
|
||||
} else if daysOverdue >= EscalationThresholdL2 {
|
||||
newLevel = 2
|
||||
} else if daysOverdue >= EscalationThresholdL1 {
|
||||
newLevel = 1
|
||||
}
|
||||
|
||||
// Only escalate if the level has increased
|
||||
if newLevel <= assignment.EscalationLevel {
|
||||
continue
|
||||
}
|
||||
|
||||
previousLevel := assignment.EscalationLevel
|
||||
|
||||
// Update the assignment
|
||||
nowTime := now
|
||||
_, err := store.pool.Exec(ctx, `
|
||||
UPDATE training_assignments SET
|
||||
escalation_level = $2,
|
||||
last_escalation_at = $3,
|
||||
status = 'overdue',
|
||||
updated_at = $3
|
||||
WHERE id = $1
|
||||
`, assignment.ID, newLevel, nowTime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Log the escalation
|
||||
assignmentID := assignment.ID
|
||||
store.LogAction(ctx, &AuditLogEntry{
|
||||
TenantID: tenantID,
|
||||
UserID: &assignment.UserID,
|
||||
Action: AuditActionEscalated,
|
||||
EntityType: AuditEntityAssignment,
|
||||
EntityID: &assignmentID,
|
||||
Details: map[string]interface{}{
|
||||
"previous_level": previousLevel,
|
||||
"new_level": newLevel,
|
||||
"days_overdue": daysOverdue,
|
||||
"label": EscalationLabels[newLevel],
|
||||
},
|
||||
})
|
||||
|
||||
results = append(results, EscalationResult{
|
||||
AssignmentID: assignment.ID,
|
||||
UserID: assignment.UserID,
|
||||
UserName: assignment.UserName,
|
||||
UserEmail: assignment.UserEmail,
|
||||
ModuleTitle: assignment.ModuleTitle,
|
||||
PreviousLevel: previousLevel,
|
||||
NewLevel: newLevel,
|
||||
DaysOverdue: daysOverdue,
|
||||
EscalationLabel: EscalationLabels[newLevel],
|
||||
})
|
||||
}
|
||||
|
||||
if results == nil {
|
||||
results = []EscalationResult{}
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// GetOverdueDeadlines returns all overdue assignments with deadline info
|
||||
func GetOverdueDeadlines(ctx context.Context, store *Store, tenantID uuid.UUID) ([]DeadlineInfo, error) {
|
||||
rows, err := store.pool.Query(ctx, `
|
||||
SELECT
|
||||
ta.id, m.module_code, m.title,
|
||||
ta.user_id, ta.user_name, ta.deadline, ta.status,
|
||||
EXTRACT(DAY FROM (NOW() - ta.deadline))::INT AS days_overdue
|
||||
FROM training_assignments ta
|
||||
JOIN training_modules m ON m.id = ta.module_id
|
||||
WHERE ta.tenant_id = $1
|
||||
AND ta.status IN ('pending', 'in_progress', 'overdue')
|
||||
AND ta.deadline < NOW()
|
||||
ORDER BY ta.deadline ASC
|
||||
`, tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var deadlines []DeadlineInfo
|
||||
for rows.Next() {
|
||||
var d DeadlineInfo
|
||||
var status string
|
||||
|
||||
err := rows.Scan(
|
||||
&d.AssignmentID, &d.ModuleCode, &d.ModuleTitle,
|
||||
&d.UserID, &d.UserName, &d.Deadline, &status,
|
||||
&d.DaysLeft,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
d.Status = AssignmentStatus(status)
|
||||
d.DaysLeft = -d.DaysLeft // Negative means overdue
|
||||
deadlines = append(deadlines, d)
|
||||
}
|
||||
|
||||
if deadlines == nil {
|
||||
deadlines = []DeadlineInfo{}
|
||||
}
|
||||
|
||||
return deadlines, nil
|
||||
}
|
||||
|
||||
// VerifyCertificate verifies a certificate by checking the assignment status
|
||||
func VerifyCertificate(ctx context.Context, store *Store, certificateID uuid.UUID) (bool, *TrainingAssignment, error) {
|
||||
// Find assignment with this certificate
|
||||
var assignmentID uuid.UUID
|
||||
err := store.pool.QueryRow(ctx,
|
||||
"SELECT id FROM training_assignments WHERE certificate_id = $1",
|
||||
certificateID).Scan(&assignmentID)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
assignment, err := store.GetAssignment(ctx, assignmentID)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
if assignment == nil {
|
||||
return false, nil, nil
|
||||
}
|
||||
|
||||
return assignment.Status == AssignmentStatusCompleted, assignment, nil
|
||||
}
|
||||
127
ai-compliance-sdk/internal/training/matrix.go
Normal file
127
ai-compliance-sdk/internal/training/matrix.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package training
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// ComputeRequiredModules returns all required training modules for a user
|
||||
// based on their assigned roles. Deduplicates modules across roles.
|
||||
func ComputeRequiredModules(ctx context.Context, store *Store, tenantID uuid.UUID, roleCodes []string) ([]TrainingModule, error) {
|
||||
seen := make(map[uuid.UUID]bool)
|
||||
var modules []TrainingModule
|
||||
|
||||
for _, role := range roleCodes {
|
||||
entries, err := store.GetMatrixForRole(ctx, tenantID, role)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if seen[entry.ModuleID] {
|
||||
continue
|
||||
}
|
||||
seen[entry.ModuleID] = true
|
||||
|
||||
module, err := store.GetModule(ctx, entry.ModuleID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if module != nil && module.IsActive {
|
||||
modules = append(modules, *module)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if modules == nil {
|
||||
modules = []TrainingModule{}
|
||||
}
|
||||
|
||||
return modules, nil
|
||||
}
|
||||
|
||||
// GetComplianceGaps finds modules that are required but not completed for a user
|
||||
func GetComplianceGaps(ctx context.Context, store *Store, tenantID uuid.UUID, userID uuid.UUID, roleCodes []string) ([]ComplianceGap, error) {
|
||||
var gaps []ComplianceGap
|
||||
|
||||
for _, role := range roleCodes {
|
||||
entries, err := store.GetMatrixForRole(ctx, tenantID, role)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
// Check if there's an active, completed assignment for this module
|
||||
assignments, _, err := store.ListAssignments(ctx, tenantID, &AssignmentFilters{
|
||||
ModuleID: &entry.ModuleID,
|
||||
UserID: &userID,
|
||||
Limit: 1,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gap := ComplianceGap{
|
||||
ModuleID: entry.ModuleID,
|
||||
ModuleCode: entry.ModuleCode,
|
||||
ModuleTitle: entry.ModuleTitle,
|
||||
RoleCode: role,
|
||||
IsMandatory: entry.IsMandatory,
|
||||
}
|
||||
|
||||
// Determine regulation area from module
|
||||
module, err := store.GetModule(ctx, entry.ModuleID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if module != nil {
|
||||
gap.RegulationArea = module.RegulationArea
|
||||
}
|
||||
|
||||
if len(assignments) == 0 {
|
||||
gap.Status = "missing"
|
||||
gaps = append(gaps, gap)
|
||||
} else {
|
||||
a := assignments[0]
|
||||
gap.AssignmentID = &a.ID
|
||||
gap.Deadline = &a.Deadline
|
||||
|
||||
switch a.Status {
|
||||
case AssignmentStatusCompleted:
|
||||
// No gap
|
||||
continue
|
||||
case AssignmentStatusOverdue, AssignmentStatusExpired:
|
||||
gap.Status = string(a.Status)
|
||||
gaps = append(gaps, gap)
|
||||
default:
|
||||
// Check if overdue
|
||||
if a.Deadline.Before(timeNow()) {
|
||||
gap.Status = "overdue"
|
||||
gaps = append(gaps, gap)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if gaps == nil {
|
||||
gaps = []ComplianceGap{}
|
||||
}
|
||||
|
||||
return gaps, nil
|
||||
}
|
||||
|
||||
// BuildMatrixResponse builds the full CTM response grouped by role
|
||||
func BuildMatrixResponse(entries []TrainingMatrixEntry) *MatrixResponse {
|
||||
resp := &MatrixResponse{
|
||||
Entries: make(map[string][]TrainingMatrixEntry),
|
||||
Roles: RoleLabels,
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
resp.Entries[entry.RoleCode] = append(resp.Entries[entry.RoleCode], entry)
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
186
ai-compliance-sdk/internal/training/media.go
Normal file
186
ai-compliance-sdk/internal/training/media.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package training
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// MediaType represents audio or video
|
||||
type MediaType string
|
||||
|
||||
const (
|
||||
MediaTypeAudio MediaType = "audio"
|
||||
MediaTypeVideo MediaType = "video"
|
||||
)
|
||||
|
||||
// MediaStatus represents the processing status
|
||||
type MediaStatus string
|
||||
|
||||
const (
|
||||
MediaStatusProcessing MediaStatus = "processing"
|
||||
MediaStatusCompleted MediaStatus = "completed"
|
||||
MediaStatusFailed MediaStatus = "failed"
|
||||
)
|
||||
|
||||
// TrainingMedia represents a generated media file
|
||||
type TrainingMedia struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
ModuleID uuid.UUID `json:"module_id"`
|
||||
ContentID *uuid.UUID `json:"content_id,omitempty"`
|
||||
MediaType MediaType `json:"media_type"`
|
||||
Status MediaStatus `json:"status"`
|
||||
Bucket string `json:"bucket"`
|
||||
ObjectKey string `json:"object_key"`
|
||||
FileSizeBytes int64 `json:"file_size_bytes"`
|
||||
DurationSeconds float64 `json:"duration_seconds"`
|
||||
MimeType string `json:"mime_type"`
|
||||
VoiceModel string `json:"voice_model"`
|
||||
Language string `json:"language"`
|
||||
Metadata json.RawMessage `json:"metadata"`
|
||||
ErrorMessage string `json:"error_message,omitempty"`
|
||||
GeneratedBy string `json:"generated_by"`
|
||||
IsPublished bool `json:"is_published"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// TTS Client
|
||||
// ============================================================================
|
||||
|
||||
// TTSClient communicates with the compliance-tts-service
|
||||
type TTSClient struct {
|
||||
baseURL string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// NewTTSClient creates a new TTS service client
|
||||
func NewTTSClient(baseURL string) *TTSClient {
|
||||
return &TTSClient{
|
||||
baseURL: baseURL,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 5 * time.Minute,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// TTSSynthesizeRequest is the request to synthesize audio
|
||||
type TTSSynthesizeRequest struct {
|
||||
Text string `json:"text"`
|
||||
Language string `json:"language"`
|
||||
Voice string `json:"voice"`
|
||||
ModuleID string `json:"module_id"`
|
||||
ContentID string `json:"content_id,omitempty"`
|
||||
}
|
||||
|
||||
// TTSSynthesizeResponse is the response from audio synthesis
|
||||
type TTSSynthesizeResponse struct {
|
||||
AudioID string `json:"audio_id"`
|
||||
Bucket string `json:"bucket"`
|
||||
ObjectKey string `json:"object_key"`
|
||||
DurationSeconds float64 `json:"duration_seconds"`
|
||||
SizeBytes int64 `json:"size_bytes"`
|
||||
}
|
||||
|
||||
// TTSGenerateVideoRequest is the request to generate a video
|
||||
type TTSGenerateVideoRequest struct {
|
||||
Script map[string]interface{} `json:"script"`
|
||||
AudioObjectKey string `json:"audio_object_key"`
|
||||
ModuleID string `json:"module_id"`
|
||||
}
|
||||
|
||||
// TTSGenerateVideoResponse is the response from video generation
|
||||
type TTSGenerateVideoResponse struct {
|
||||
VideoID string `json:"video_id"`
|
||||
Bucket string `json:"bucket"`
|
||||
ObjectKey string `json:"object_key"`
|
||||
DurationSeconds float64 `json:"duration_seconds"`
|
||||
SizeBytes int64 `json:"size_bytes"`
|
||||
}
|
||||
|
||||
// Synthesize calls the TTS service to create audio
|
||||
func (c *TTSClient) Synthesize(ctx context.Context, req *TTSSynthesizeRequest) (*TTSSynthesizeResponse, error) {
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal request: %w", err)
|
||||
}
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+"/synthesize", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create request: %w", err)
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("TTS service request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("TTS service error (%d): %s", resp.StatusCode, string(respBody))
|
||||
}
|
||||
|
||||
var result TTSSynthesizeResponse
|
||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||
return nil, fmt.Errorf("parse TTS response: %w", err)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// GenerateVideo calls the TTS service to create a presentation video
|
||||
func (c *TTSClient) GenerateVideo(ctx context.Context, req *TTSGenerateVideoRequest) (*TTSGenerateVideoResponse, error) {
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal request: %w", err)
|
||||
}
|
||||
|
||||
httpReq, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+"/generate-video", bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create request: %w", err)
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("TTS service request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("TTS service error (%d): %s", resp.StatusCode, string(respBody))
|
||||
}
|
||||
|
||||
var result TTSGenerateVideoResponse
|
||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||
return nil, fmt.Errorf("parse TTS response: %w", err)
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// IsHealthy checks if the TTS service is responsive
|
||||
func (c *TTSClient) IsHealthy(ctx context.Context) bool {
|
||||
httpReq, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+"/health", nil)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
resp, err := c.httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return resp.StatusCode == http.StatusOK
|
||||
}
|
||||
500
ai-compliance-sdk/internal/training/models.go
Normal file
500
ai-compliance-sdk/internal/training/models.go
Normal file
@@ -0,0 +1,500 @@
|
||||
package training
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// Constants / Enums
|
||||
// ============================================================================
|
||||
|
||||
// RegulationArea represents a compliance regulation area
|
||||
type RegulationArea string
|
||||
|
||||
const (
|
||||
RegulationDSGVO RegulationArea = "dsgvo"
|
||||
RegulationNIS2 RegulationArea = "nis2"
|
||||
RegulationISO27001 RegulationArea = "iso27001"
|
||||
RegulationAIAct RegulationArea = "ai_act"
|
||||
RegulationGeschGehG RegulationArea = "geschgehg"
|
||||
RegulationHinSchG RegulationArea = "hinschg"
|
||||
)
|
||||
|
||||
// FrequencyType represents the training frequency
|
||||
type FrequencyType string
|
||||
|
||||
const (
|
||||
FrequencyOnboarding FrequencyType = "onboarding"
|
||||
FrequencyAnnual FrequencyType = "annual"
|
||||
FrequencyEventTrigger FrequencyType = "event_trigger"
|
||||
FrequencyMicro FrequencyType = "micro"
|
||||
)
|
||||
|
||||
// AssignmentStatus represents the status of a training assignment
|
||||
type AssignmentStatus string
|
||||
|
||||
const (
|
||||
AssignmentStatusPending AssignmentStatus = "pending"
|
||||
AssignmentStatusInProgress AssignmentStatus = "in_progress"
|
||||
AssignmentStatusCompleted AssignmentStatus = "completed"
|
||||
AssignmentStatusOverdue AssignmentStatus = "overdue"
|
||||
AssignmentStatusExpired AssignmentStatus = "expired"
|
||||
)
|
||||
|
||||
// TriggerType represents how a training was assigned
|
||||
type TriggerType string
|
||||
|
||||
const (
|
||||
TriggerOnboarding TriggerType = "onboarding"
|
||||
TriggerAnnual TriggerType = "annual"
|
||||
TriggerEvent TriggerType = "event"
|
||||
TriggerManual TriggerType = "manual"
|
||||
)
|
||||
|
||||
// ContentFormat represents the format of module content
|
||||
type ContentFormat string
|
||||
|
||||
const (
|
||||
ContentFormatMarkdown ContentFormat = "markdown"
|
||||
ContentFormatHTML ContentFormat = "html"
|
||||
)
|
||||
|
||||
// Difficulty represents the difficulty level of a quiz question
|
||||
type Difficulty string
|
||||
|
||||
const (
|
||||
DifficultyEasy Difficulty = "easy"
|
||||
DifficultyMedium Difficulty = "medium"
|
||||
DifficultyHard Difficulty = "hard"
|
||||
)
|
||||
|
||||
// AuditAction represents an action in the audit trail
|
||||
type AuditAction string
|
||||
|
||||
const (
|
||||
AuditActionAssigned AuditAction = "assigned"
|
||||
AuditActionStarted AuditAction = "started"
|
||||
AuditActionCompleted AuditAction = "completed"
|
||||
AuditActionQuizSubmitted AuditAction = "quiz_submitted"
|
||||
AuditActionEscalated AuditAction = "escalated"
|
||||
AuditActionCertificateIssued AuditAction = "certificate_issued"
|
||||
AuditActionContentGenerated AuditAction = "content_generated"
|
||||
)
|
||||
|
||||
// AuditEntityType represents the type of entity in audit log
|
||||
type AuditEntityType string
|
||||
|
||||
const (
|
||||
AuditEntityAssignment AuditEntityType = "assignment"
|
||||
AuditEntityModule AuditEntityType = "module"
|
||||
AuditEntityQuiz AuditEntityType = "quiz"
|
||||
AuditEntityCertificate AuditEntityType = "certificate"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// Role Constants
|
||||
// ============================================================================
|
||||
|
||||
const (
|
||||
RoleR1 = "R1" // Geschaeftsfuehrung
|
||||
RoleR2 = "R2" // IT-Leitung
|
||||
RoleR3 = "R3" // DSB
|
||||
RoleR4 = "R4" // ISB
|
||||
RoleR5 = "R5" // HR
|
||||
RoleR6 = "R6" // Einkauf
|
||||
RoleR7 = "R7" // Fachabteilung
|
||||
RoleR8 = "R8" // IT-Admin
|
||||
RoleR9 = "R9" // Alle Mitarbeiter
|
||||
)
|
||||
|
||||
// RoleLabels maps role codes to human-readable labels
|
||||
var RoleLabels = map[string]string{
|
||||
RoleR1: "Geschaeftsfuehrung",
|
||||
RoleR2: "IT-Leitung",
|
||||
RoleR3: "Datenschutzbeauftragter",
|
||||
RoleR4: "Informationssicherheitsbeauftragter",
|
||||
RoleR5: "HR / Personal",
|
||||
RoleR6: "Einkauf / Beschaffung",
|
||||
RoleR7: "Fachabteilung",
|
||||
RoleR8: "IT-Administration",
|
||||
RoleR9: "Alle Mitarbeiter",
|
||||
}
|
||||
|
||||
// NIS2RoleMapping maps internal roles to NIS2 levels
|
||||
var NIS2RoleMapping = map[string]string{
|
||||
RoleR1: "N1", // Geschaeftsfuehrung
|
||||
RoleR2: "N2", // IT-Leitung
|
||||
RoleR3: "N3", // DSB
|
||||
RoleR4: "N3", // ISB
|
||||
RoleR5: "N4", // HR
|
||||
RoleR6: "N4", // Einkauf
|
||||
RoleR7: "N5", // Fachabteilung
|
||||
RoleR8: "N2", // IT-Admin
|
||||
RoleR9: "N5", // Alle Mitarbeiter
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Main Entities
|
||||
// ============================================================================
|
||||
|
||||
// TrainingModule represents a compliance training module
|
||||
type TrainingModule struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
TenantID uuid.UUID `json:"tenant_id"`
|
||||
AcademyCourseID *uuid.UUID `json:"academy_course_id,omitempty"`
|
||||
ModuleCode string `json:"module_code"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description,omitempty"`
|
||||
RegulationArea RegulationArea `json:"regulation_area"`
|
||||
NIS2Relevant bool `json:"nis2_relevant"`
|
||||
ISOControls []string `json:"iso_controls"` // JSONB
|
||||
FrequencyType FrequencyType `json:"frequency_type"`
|
||||
ValidityDays int `json:"validity_days"`
|
||||
RiskWeight float64 `json:"risk_weight"`
|
||||
ContentType string `json:"content_type"`
|
||||
DurationMinutes int `json:"duration_minutes"`
|
||||
PassThreshold int `json:"pass_threshold"`
|
||||
IsActive bool `json:"is_active"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// TrainingMatrixEntry represents a role-to-module mapping in the CTM
|
||||
type TrainingMatrixEntry struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
TenantID uuid.UUID `json:"tenant_id"`
|
||||
RoleCode string `json:"role_code"`
|
||||
ModuleID uuid.UUID `json:"module_id"`
|
||||
IsMandatory bool `json:"is_mandatory"`
|
||||
Priority int `json:"priority"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
// Joined fields (optional, populated in queries)
|
||||
ModuleCode string `json:"module_code,omitempty"`
|
||||
ModuleTitle string `json:"module_title,omitempty"`
|
||||
}
|
||||
|
||||
// TrainingAssignment represents a user's training assignment
|
||||
type TrainingAssignment struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
TenantID uuid.UUID `json:"tenant_id"`
|
||||
ModuleID uuid.UUID `json:"module_id"`
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
UserName string `json:"user_name"`
|
||||
UserEmail string `json:"user_email"`
|
||||
RoleCode string `json:"role_code,omitempty"`
|
||||
TriggerType TriggerType `json:"trigger_type"`
|
||||
TriggerEvent string `json:"trigger_event,omitempty"`
|
||||
Status AssignmentStatus `json:"status"`
|
||||
ProgressPercent int `json:"progress_percent"`
|
||||
QuizScore *float64 `json:"quiz_score,omitempty"`
|
||||
QuizPassed *bool `json:"quiz_passed,omitempty"`
|
||||
QuizAttempts int `json:"quiz_attempts"`
|
||||
StartedAt *time.Time `json:"started_at,omitempty"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||
Deadline time.Time `json:"deadline"`
|
||||
CertificateID *uuid.UUID `json:"certificate_id,omitempty"`
|
||||
EscalationLevel int `json:"escalation_level"`
|
||||
LastEscalationAt *time.Time `json:"last_escalation_at,omitempty"`
|
||||
EnrollmentID *uuid.UUID `json:"enrollment_id,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
// Joined fields
|
||||
ModuleCode string `json:"module_code,omitempty"`
|
||||
ModuleTitle string `json:"module_title,omitempty"`
|
||||
}
|
||||
|
||||
// QuizQuestion represents a persistent quiz question for a module
|
||||
type QuizQuestion struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
ModuleID uuid.UUID `json:"module_id"`
|
||||
Question string `json:"question"`
|
||||
Options []string `json:"options"` // JSONB
|
||||
CorrectIndex int `json:"correct_index"`
|
||||
Explanation string `json:"explanation,omitempty"`
|
||||
Difficulty Difficulty `json:"difficulty"`
|
||||
IsActive bool `json:"is_active"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// QuizAttempt represents a single quiz attempt by a user
|
||||
type QuizAttempt struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
AssignmentID uuid.UUID `json:"assignment_id"`
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
Answers []QuizAnswer `json:"answers"` // JSONB
|
||||
Score float64 `json:"score"`
|
||||
Passed bool `json:"passed"`
|
||||
CorrectCount int `json:"correct_count"`
|
||||
TotalCount int `json:"total_count"`
|
||||
DurationSeconds *int `json:"duration_seconds,omitempty"`
|
||||
AttemptedAt time.Time `json:"attempted_at"`
|
||||
}
|
||||
|
||||
// QuizAnswer represents a single answer within a quiz attempt
|
||||
type QuizAnswer struct {
|
||||
QuestionID uuid.UUID `json:"question_id"`
|
||||
SelectedIndex int `json:"selected_index"`
|
||||
Correct bool `json:"correct"`
|
||||
}
|
||||
|
||||
// AuditLogEntry represents an entry in the training audit trail
|
||||
type AuditLogEntry struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
TenantID uuid.UUID `json:"tenant_id"`
|
||||
UserID *uuid.UUID `json:"user_id,omitempty"`
|
||||
Action AuditAction `json:"action"`
|
||||
EntityType AuditEntityType `json:"entity_type"`
|
||||
EntityID *uuid.UUID `json:"entity_id,omitempty"`
|
||||
Details map[string]interface{} `json:"details"` // JSONB
|
||||
IPAddress string `json:"ip_address,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// ModuleContent represents LLM-generated or manual content for a module
|
||||
type ModuleContent struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
ModuleID uuid.UUID `json:"module_id"`
|
||||
Version int `json:"version"`
|
||||
ContentFormat ContentFormat `json:"content_format"`
|
||||
ContentBody string `json:"content_body"`
|
||||
Summary string `json:"summary,omitempty"`
|
||||
GeneratedBy string `json:"generated_by,omitempty"`
|
||||
LLMModel string `json:"llm_model,omitempty"`
|
||||
IsPublished bool `json:"is_published"`
|
||||
ReviewedBy *uuid.UUID `json:"reviewed_by,omitempty"`
|
||||
ReviewedAt *time.Time `json:"reviewed_at,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// TrainingStats contains aggregated training metrics
|
||||
type TrainingStats struct {
|
||||
TotalModules int `json:"total_modules"`
|
||||
TotalAssignments int `json:"total_assignments"`
|
||||
CompletionRate float64 `json:"completion_rate"`
|
||||
OverdueCount int `json:"overdue_count"`
|
||||
PendingCount int `json:"pending_count"`
|
||||
InProgressCount int `json:"in_progress_count"`
|
||||
CompletedCount int `json:"completed_count"`
|
||||
AvgQuizScore float64 `json:"avg_quiz_score"`
|
||||
AvgCompletionDays float64 `json:"avg_completion_days"`
|
||||
UpcomingDeadlines int `json:"upcoming_deadlines"` // within 7 days
|
||||
}
|
||||
|
||||
// ComplianceGap represents a missing or overdue training requirement
|
||||
type ComplianceGap struct {
|
||||
ModuleID uuid.UUID `json:"module_id"`
|
||||
ModuleCode string `json:"module_code"`
|
||||
ModuleTitle string `json:"module_title"`
|
||||
RegulationArea RegulationArea `json:"regulation_area"`
|
||||
RoleCode string `json:"role_code"`
|
||||
IsMandatory bool `json:"is_mandatory"`
|
||||
AssignmentID *uuid.UUID `json:"assignment_id,omitempty"`
|
||||
Status string `json:"status"` // "missing", "overdue", "expired"
|
||||
Deadline *time.Time `json:"deadline,omitempty"`
|
||||
}
|
||||
|
||||
// EscalationResult represents the result of an escalation check
|
||||
type EscalationResult struct {
|
||||
AssignmentID uuid.UUID `json:"assignment_id"`
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
UserName string `json:"user_name"`
|
||||
UserEmail string `json:"user_email"`
|
||||
ModuleTitle string `json:"module_title"`
|
||||
PreviousLevel int `json:"previous_level"`
|
||||
NewLevel int `json:"new_level"`
|
||||
DaysOverdue int `json:"days_overdue"`
|
||||
EscalationLabel string `json:"escalation_label"`
|
||||
}
|
||||
|
||||
// DeadlineInfo represents upcoming deadline information
|
||||
type DeadlineInfo struct {
|
||||
AssignmentID uuid.UUID `json:"assignment_id"`
|
||||
ModuleCode string `json:"module_code"`
|
||||
ModuleTitle string `json:"module_title"`
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
UserName string `json:"user_name"`
|
||||
Deadline time.Time `json:"deadline"`
|
||||
DaysLeft int `json:"days_left"`
|
||||
Status AssignmentStatus `json:"status"`
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Filter Types
|
||||
// ============================================================================
|
||||
|
||||
// ModuleFilters defines filters for listing modules
|
||||
type ModuleFilters struct {
|
||||
RegulationArea RegulationArea
|
||||
FrequencyType FrequencyType
|
||||
IsActive *bool
|
||||
NIS2Relevant *bool
|
||||
Search string
|
||||
Limit int
|
||||
Offset int
|
||||
}
|
||||
|
||||
// AssignmentFilters defines filters for listing assignments
|
||||
type AssignmentFilters struct {
|
||||
ModuleID *uuid.UUID
|
||||
UserID *uuid.UUID
|
||||
RoleCode string
|
||||
Status AssignmentStatus
|
||||
Overdue *bool
|
||||
Limit int
|
||||
Offset int
|
||||
}
|
||||
|
||||
// AuditLogFilters defines filters for listing audit log entries
|
||||
type AuditLogFilters struct {
|
||||
UserID *uuid.UUID
|
||||
Action AuditAction
|
||||
EntityType AuditEntityType
|
||||
Limit int
|
||||
Offset int
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// API Request/Response Types
|
||||
// ============================================================================
|
||||
|
||||
// CreateModuleRequest is the API request for creating a training module
|
||||
type CreateModuleRequest struct {
|
||||
ModuleCode string `json:"module_code" binding:"required"`
|
||||
Title string `json:"title" binding:"required"`
|
||||
Description string `json:"description,omitempty"`
|
||||
RegulationArea RegulationArea `json:"regulation_area" binding:"required"`
|
||||
NIS2Relevant bool `json:"nis2_relevant"`
|
||||
ISOControls []string `json:"iso_controls,omitempty"`
|
||||
FrequencyType FrequencyType `json:"frequency_type" binding:"required"`
|
||||
ValidityDays int `json:"validity_days"`
|
||||
RiskWeight float64 `json:"risk_weight"`
|
||||
ContentType string `json:"content_type"`
|
||||
DurationMinutes int `json:"duration_minutes"`
|
||||
PassThreshold int `json:"pass_threshold"`
|
||||
}
|
||||
|
||||
// UpdateModuleRequest is the API request for updating a training module
|
||||
type UpdateModuleRequest struct {
|
||||
Title *string `json:"title,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
NIS2Relevant *bool `json:"nis2_relevant,omitempty"`
|
||||
ISOControls []string `json:"iso_controls,omitempty"`
|
||||
ValidityDays *int `json:"validity_days,omitempty"`
|
||||
RiskWeight *float64 `json:"risk_weight,omitempty"`
|
||||
DurationMinutes *int `json:"duration_minutes,omitempty"`
|
||||
PassThreshold *int `json:"pass_threshold,omitempty"`
|
||||
IsActive *bool `json:"is_active,omitempty"`
|
||||
}
|
||||
|
||||
// SetMatrixEntryRequest is the API request for setting a CTM entry
|
||||
type SetMatrixEntryRequest struct {
|
||||
RoleCode string `json:"role_code" binding:"required"`
|
||||
ModuleID uuid.UUID `json:"module_id" binding:"required"`
|
||||
IsMandatory bool `json:"is_mandatory"`
|
||||
Priority int `json:"priority"`
|
||||
}
|
||||
|
||||
// ComputeAssignmentsRequest is the API request for computing assignments
|
||||
type ComputeAssignmentsRequest struct {
|
||||
UserID uuid.UUID `json:"user_id" binding:"required"`
|
||||
UserName string `json:"user_name" binding:"required"`
|
||||
UserEmail string `json:"user_email" binding:"required"`
|
||||
Roles []string `json:"roles" binding:"required"`
|
||||
Trigger string `json:"trigger"`
|
||||
}
|
||||
|
||||
// UpdateAssignmentProgressRequest updates progress on an assignment
|
||||
type UpdateAssignmentProgressRequest struct {
|
||||
Progress int `json:"progress" binding:"required"`
|
||||
}
|
||||
|
||||
// SubmitTrainingQuizRequest is the API request for submitting a quiz
|
||||
type SubmitTrainingQuizRequest struct {
|
||||
AssignmentID uuid.UUID `json:"assignment_id" binding:"required"`
|
||||
Answers []QuizAnswer `json:"answers" binding:"required"`
|
||||
DurationSeconds *int `json:"duration_seconds,omitempty"`
|
||||
}
|
||||
|
||||
// SubmitTrainingQuizResponse is the API response for quiz submission
|
||||
type SubmitTrainingQuizResponse struct {
|
||||
AttemptID uuid.UUID `json:"attempt_id"`
|
||||
Score float64 `json:"score"`
|
||||
Passed bool `json:"passed"`
|
||||
CorrectCount int `json:"correct_count"`
|
||||
TotalCount int `json:"total_count"`
|
||||
Threshold int `json:"threshold"`
|
||||
}
|
||||
|
||||
// GenerateContentRequest is the API request for LLM content generation
|
||||
type GenerateContentRequest struct {
|
||||
ModuleID uuid.UUID `json:"module_id" binding:"required"`
|
||||
Language string `json:"language"`
|
||||
}
|
||||
|
||||
// GenerateQuizRequest is the API request for LLM quiz generation
|
||||
type GenerateQuizRequest struct {
|
||||
ModuleID uuid.UUID `json:"module_id" binding:"required"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
// PublishContentRequest is the API request for publishing content
|
||||
type PublishContentRequest struct {
|
||||
ReviewedBy uuid.UUID `json:"reviewed_by"`
|
||||
}
|
||||
|
||||
// BulkAssignRequest is the API request for bulk assigning a module
|
||||
type BulkAssignRequest struct {
|
||||
ModuleID uuid.UUID `json:"module_id" binding:"required"`
|
||||
RoleCodes []string `json:"role_codes" binding:"required"`
|
||||
Trigger string `json:"trigger"`
|
||||
Deadline time.Time `json:"deadline" binding:"required"`
|
||||
}
|
||||
|
||||
// ModuleListResponse is the API response for listing modules
|
||||
type ModuleListResponse struct {
|
||||
Modules []TrainingModule `json:"modules"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
// AssignmentListResponse is the API response for listing assignments
|
||||
type AssignmentListResponse struct {
|
||||
Assignments []TrainingAssignment `json:"assignments"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
// MatrixResponse is the API response for the full training matrix
|
||||
type MatrixResponse struct {
|
||||
Entries map[string][]TrainingMatrixEntry `json:"entries"` // role_code -> entries
|
||||
Roles map[string]string `json:"roles"` // role_code -> label
|
||||
}
|
||||
|
||||
// AuditLogResponse is the API response for listing audit log entries
|
||||
type AuditLogResponse struct {
|
||||
Entries []AuditLogEntry `json:"entries"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
// EscalationResponse is the API response for escalation check
|
||||
type EscalationResponse struct {
|
||||
Results []EscalationResult `json:"results"`
|
||||
TotalChecked int `json:"total_checked"`
|
||||
Escalated int `json:"escalated"`
|
||||
}
|
||||
|
||||
// DeadlineListResponse is the API response for listing deadlines
|
||||
type DeadlineListResponse struct {
|
||||
Deadlines []DeadlineInfo `json:"deadlines"`
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
// BulkResult holds the result of a bulk generation operation
|
||||
type BulkResult struct {
|
||||
Generated int `json:"generated"`
|
||||
Skipped int `json:"skipped"`
|
||||
Errors []string `json:"errors"`
|
||||
}
|
||||
1277
ai-compliance-sdk/internal/training/store.go
Normal file
1277
ai-compliance-sdk/internal/training/store.go
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user