package handlers import ( "bytes" "crypto/sha256" "encoding/hex" "fmt" "net/http" "strings" "time" "github.com/breakpilot/ai-compliance-sdk/internal/llm" "github.com/breakpilot/ai-compliance-sdk/internal/rbac" "github.com/breakpilot/ai-compliance-sdk/internal/ucca" "github.com/gin-gonic/gin" "github.com/google/uuid" ) // UCCAHandlers handles UCCA-related API endpoints type UCCAHandlers struct { store *ucca.Store escalationStore *ucca.EscalationStore policyEngine *ucca.PolicyEngine legacyRuleEngine *ucca.RuleEngine // Keep for backwards compatibility providerRegistry *llm.ProviderRegistry legalRAGClient *ucca.LegalRAGClient escalationTrigger *ucca.EscalationTrigger } // NewUCCAHandlers creates new UCCA handlers func NewUCCAHandlers(store *ucca.Store, escalationStore *ucca.EscalationStore, providerRegistry *llm.ProviderRegistry) *UCCAHandlers { // Try to create YAML-based policy engine first policyEngine, err := ucca.NewPolicyEngine() if err != nil { // Log warning but don't fail - fall back to legacy engine fmt.Printf("Warning: Could not load YAML policy engine: %v. Falling back to legacy rules.\n", err) } return &UCCAHandlers{ store: store, escalationStore: escalationStore, policyEngine: policyEngine, // May be nil if YAML loading failed legacyRuleEngine: ucca.NewRuleEngine(), providerRegistry: providerRegistry, legalRAGClient: ucca.NewLegalRAGClient(), escalationTrigger: ucca.DefaultEscalationTrigger(), } } // ============================================================================ // POST /sdk/v1/ucca/assess - Evaluate a use case // ============================================================================ // Assess evaluates a use case intake and creates an assessment func (h *UCCAHandlers) Assess(c *gin.Context) { tenantID := rbac.GetTenantID(c) userID := rbac.GetUserID(c) if tenantID == uuid.Nil { c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"}) return } var intake ucca.UseCaseIntake if err := c.ShouldBindJSON(&intake); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Run evaluation - prefer YAML-based policy engine if available var result *ucca.AssessmentResult var policyVersion string if h.policyEngine != nil { result = h.policyEngine.Evaluate(&intake) policyVersion = h.policyEngine.GetPolicyVersion() } else { result = h.legacyRuleEngine.Evaluate(&intake) policyVersion = "1.0.0-legacy" } // Calculate hash of use case text hash := sha256.Sum256([]byte(intake.UseCaseText)) hashStr := hex.EncodeToString(hash[:]) // Create assessment record assessment := &ucca.Assessment{ TenantID: tenantID, Title: intake.Title, PolicyVersion: policyVersion, Status: "completed", Intake: intake, UseCaseTextStored: intake.StoreRawText, UseCaseTextHash: hashStr, Feasibility: result.Feasibility, RiskLevel: result.RiskLevel, Complexity: result.Complexity, RiskScore: result.RiskScore, TriggeredRules: result.TriggeredRules, RequiredControls: result.RequiredControls, RecommendedArchitecture: result.RecommendedArchitecture, ForbiddenPatterns: result.ForbiddenPatterns, ExampleMatches: result.ExampleMatches, DSFARecommended: result.DSFARecommended, Art22Risk: result.Art22Risk, TrainingAllowed: result.TrainingAllowed, Domain: intake.Domain, CreatedBy: userID, } // Clear use case text if not opted in to store if !intake.StoreRawText { assessment.Intake.UseCaseText = "" } // Generate title if not provided if assessment.Title == "" { assessment.Title = fmt.Sprintf("Assessment vom %s", time.Now().Format("02.01.2006 15:04")) } // Save to database if err := h.store.CreateAssessment(c.Request.Context(), assessment); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } // Automatically create escalation based on assessment result var escalation *ucca.Escalation if h.escalationStore != nil && h.escalationTrigger != nil { level, reason := h.escalationTrigger.DetermineEscalationLevel(result) // Calculate due date based on SLA responseHours, _ := ucca.GetDefaultSLA(level) var dueDate *time.Time if responseHours > 0 { due := time.Now().UTC().Add(time.Duration(responseHours) * time.Hour) dueDate = &due } escalation = &ucca.Escalation{ TenantID: tenantID, AssessmentID: assessment.ID, EscalationLevel: level, EscalationReason: reason, Status: ucca.EscalationStatusPending, DueDate: dueDate, } // For E0, auto-approve if level == ucca.EscalationLevelE0 { escalation.Status = ucca.EscalationStatusApproved approveDecision := ucca.EscalationDecisionApprove escalation.Decision = &approveDecision now := time.Now().UTC() escalation.DecisionAt = &now autoNotes := "Automatische Freigabe (E0)" escalation.DecisionNotes = &autoNotes } if err := h.escalationStore.CreateEscalation(c.Request.Context(), escalation); err != nil { // Log error but don't fail the assessment creation fmt.Printf("Warning: Could not create escalation: %v\n", err) escalation = nil } else { // Add history entry h.escalationStore.AddEscalationHistory(c.Request.Context(), &ucca.EscalationHistory{ EscalationID: escalation.ID, Action: "auto_created", NewStatus: string(escalation.Status), NewLevel: string(escalation.EscalationLevel), ActorID: userID, Notes: "Automatisch erstellt bei Assessment", }) // For E1/E2/E3, try to auto-assign if level != ucca.EscalationLevelE0 { role := ucca.GetRoleForLevel(level) reviewer, err := h.escalationStore.GetNextAvailableReviewer(c.Request.Context(), tenantID, role) if err == nil && reviewer != nil { h.escalationStore.AssignEscalation(c.Request.Context(), escalation.ID, reviewer.UserID, role) h.escalationStore.IncrementReviewerCount(c.Request.Context(), reviewer.UserID) h.escalationStore.AddEscalationHistory(c.Request.Context(), &ucca.EscalationHistory{ EscalationID: escalation.ID, Action: "auto_assigned", OldStatus: string(ucca.EscalationStatusPending), NewStatus: string(ucca.EscalationStatusAssigned), ActorID: userID, Notes: "Automatisch zugewiesen an: " + reviewer.UserName, }) } } } } c.JSON(http.StatusCreated, ucca.AssessResponse{ Assessment: *assessment, Result: *result, Escalation: escalation, }) } // ============================================================================ // GET /sdk/v1/ucca/assessments - List all assessments // ============================================================================ // ListAssessments returns all assessments for a tenant func (h *UCCAHandlers) ListAssessments(c *gin.Context) { tenantID := rbac.GetTenantID(c) if tenantID == uuid.Nil { c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"}) return } filters := &ucca.AssessmentFilters{ Feasibility: c.Query("feasibility"), Domain: c.Query("domain"), RiskLevel: c.Query("risk_level"), } assessments, err := h.store.ListAssessments(c.Request.Context(), tenantID, filters) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"assessments": assessments}) } // ============================================================================ // GET /sdk/v1/ucca/assessments/:id - Get single assessment // ============================================================================ // GetAssessment returns a single assessment by ID func (h *UCCAHandlers) GetAssessment(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"}) return } assessment, err := h.store.GetAssessment(c.Request.Context(), id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if assessment == nil { c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) return } c.JSON(http.StatusOK, assessment) } // ============================================================================ // DELETE /sdk/v1/ucca/assessments/:id - Delete assessment // ============================================================================ // DeleteAssessment deletes an assessment func (h *UCCAHandlers) DeleteAssessment(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"}) return } if err := h.store.DeleteAssessment(c.Request.Context(), id); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, gin.H{"message": "deleted"}) } // ============================================================================ // GET /sdk/v1/ucca/patterns - Get pattern catalog // ============================================================================ // ListPatterns returns all available architecture patterns func (h *UCCAHandlers) ListPatterns(c *gin.Context) { var response []gin.H // Prefer YAML-based patterns if available if h.policyEngine != nil { yamlPatterns := h.policyEngine.GetAllPatterns() response = make([]gin.H, 0, len(yamlPatterns)) for _, p := range yamlPatterns { response = append(response, gin.H{ "id": p.ID, "title": p.Title, "description": p.Description, "benefit": p.Benefit, "effort": p.Effort, "risk_reduction": p.RiskReduction, }) } } else { // Fall back to legacy patterns patterns := ucca.GetAllPatterns() response = make([]gin.H, len(patterns)) for i, p := range patterns { response[i] = gin.H{ "id": p.ID, "title": p.Title, "title_de": p.TitleDE, "description": p.Description, "description_de": p.DescriptionDE, "benefits": p.Benefits, "requirements": p.Requirements, } } } c.JSON(http.StatusOK, gin.H{"patterns": response}) } // ============================================================================ // GET /sdk/v1/ucca/controls - Get control catalog // ============================================================================ // ListControls returns all available compliance controls func (h *UCCAHandlers) ListControls(c *gin.Context) { var response []gin.H // Prefer YAML-based controls if available if h.policyEngine != nil { yamlControls := h.policyEngine.GetAllControls() response = make([]gin.H, 0, len(yamlControls)) for _, ctrl := range yamlControls { response = append(response, gin.H{ "id": ctrl.ID, "title": ctrl.Title, "description": ctrl.Description, "gdpr_ref": ctrl.GDPRRef, "effort": ctrl.Effort, }) } } else { // Fall back to legacy controls for id, ctrl := range ucca.ControlLibrary { response = append(response, gin.H{ "id": id, "title": ctrl.Title, "description": ctrl.Description, "severity": ctrl.Severity, "category": ctrl.Category, "gdpr_ref": ctrl.GDPRRef, }) } } c.JSON(http.StatusOK, gin.H{"controls": response}) } // ============================================================================ // GET /sdk/v1/ucca/problem-solutions - Get problem-solution mappings // ============================================================================ // ListProblemSolutions returns all problem-solution mappings func (h *UCCAHandlers) ListProblemSolutions(c *gin.Context) { if h.policyEngine == nil { c.JSON(http.StatusOK, gin.H{ "problem_solutions": []gin.H{}, "message": "Problem-solutions only available with YAML policy engine", }) return } problemSolutions := h.policyEngine.GetProblemSolutions() response := make([]gin.H, len(problemSolutions)) for i, ps := range problemSolutions { solutions := make([]gin.H, len(ps.Solutions)) for j, sol := range ps.Solutions { solutions[j] = gin.H{ "id": sol.ID, "title": sol.Title, "pattern": sol.Pattern, "control": sol.Control, "removes_problem": sol.RemovesProblem, "team_question": sol.TeamQuestion, } } triggers := make([]gin.H, len(ps.Triggers)) for j, t := range ps.Triggers { triggers[j] = gin.H{ "rule": t.Rule, "without_control": t.WithoutControl, } } response[i] = gin.H{ "problem_id": ps.ProblemID, "title": ps.Title, "triggers": triggers, "solutions": solutions, } } c.JSON(http.StatusOK, gin.H{"problem_solutions": response}) } // ============================================================================ // GET /sdk/v1/ucca/examples - Get example catalog // ============================================================================ // ListExamples returns all available didactic examples func (h *UCCAHandlers) ListExamples(c *gin.Context) { examples := ucca.GetAllExamples() // Convert to API response format response := make([]gin.H, len(examples)) for i, ex := range examples { response[i] = gin.H{ "id": ex.ID, "title": ex.Title, "title_de": ex.TitleDE, "description": ex.Description, "description_de": ex.DescriptionDE, "domain": ex.Domain, "outcome": ex.Outcome, "outcome_de": ex.OutcomeDE, "lessons": ex.Lessons, "lessons_de": ex.LessonsDE, } } c.JSON(http.StatusOK, gin.H{"examples": response}) } // ============================================================================ // GET /sdk/v1/ucca/rules - Get all rules (transparency) // ============================================================================ // ListRules returns all rules for transparency func (h *UCCAHandlers) ListRules(c *gin.Context) { var response []gin.H var policyVersion string // Prefer YAML-based rules if available if h.policyEngine != nil { yamlRules := h.policyEngine.GetAllRules() policyVersion = h.policyEngine.GetPolicyVersion() response = make([]gin.H, len(yamlRules)) for i, r := range yamlRules { response[i] = gin.H{ "code": r.ID, "category": r.Category, "title": r.Title, "description": r.Description, "severity": r.Severity, "gdpr_ref": r.GDPRRef, "rationale": r.Rationale, "controls": r.Effect.ControlsAdd, "patterns": r.Effect.SuggestedPatterns, "risk_add": r.Effect.RiskAdd, } } } else { // Fall back to legacy rules rules := h.legacyRuleEngine.GetRules() policyVersion = "1.0.0-legacy" response = make([]gin.H, len(rules)) for i, r := range rules { response[i] = gin.H{ "code": r.Code, "category": r.Category, "title": r.Title, "title_de": r.TitleDE, "description": r.Description, "description_de": r.DescriptionDE, "severity": r.Severity, "score_delta": r.ScoreDelta, "gdpr_ref": r.GDPRRef, "controls": r.Controls, "patterns": r.Patterns, } } } c.JSON(http.StatusOK, gin.H{ "rules": response, "total": len(response), "policy_version": policyVersion, "categories": []string{ "A. Datenklassifikation", "B. Zweck & Rechtsgrundlage", "C. Automatisierung", "D. Training & Modell", "E. Hosting", "F. Domain-spezifisch", "G. Aggregation", }, }) } // ============================================================================ // POST /sdk/v1/ucca/assessments/:id/explain - Generate LLM explanation // ============================================================================ // Explain generates an LLM explanation for an assessment func (h *UCCAHandlers) Explain(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"}) return } var req ucca.ExplainRequest if err := c.ShouldBindJSON(&req); err != nil { // Default to German req.Language = "de" } if req.Language == "" { req.Language = "de" } // Get assessment assessment, err := h.store.GetAssessment(c.Request.Context(), id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if assessment == nil { c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) return } // Get legal context from RAG var legalContext *ucca.LegalContext var legalContextStr string if h.legalRAGClient != nil { legalContext, err = h.legalRAGClient.GetLegalContextForAssessment(c.Request.Context(), assessment) if err != nil { // Log error but continue without legal context fmt.Printf("Warning: Could not get legal context: %v\n", err) } else { legalContextStr = h.legalRAGClient.FormatLegalContextForPrompt(legalContext) } } // Build prompt for LLM with legal context prompt := buildExplanationPrompt(assessment, req.Language, legalContextStr) // Call LLM chatReq := &llm.ChatRequest{ Messages: []llm.Message{ {Role: "system", Content: "Du bist ein Datenschutz-Experte, der DSGVO-Compliance-Bewertungen erklärt. Antworte klar, präzise und auf Deutsch. Beziehe dich auf die angegebenen Rechtsgrundlagen."}, {Role: "user", Content: prompt}, }, MaxTokens: 2000, Temperature: 0.3, } response, err := h.providerRegistry.Chat(c.Request.Context(), chatReq) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "LLM call failed: " + err.Error()}) return } explanation := response.Message.Content model := response.Model // Save explanation to database if err := h.store.UpdateExplanation(c.Request.Context(), id, explanation, model); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, ucca.ExplainResponse{ ExplanationText: explanation, GeneratedAt: time.Now().UTC(), Model: model, LegalContext: legalContext, }) } // buildExplanationPrompt creates the prompt for the LLM explanation func buildExplanationPrompt(assessment *ucca.Assessment, language string, legalContext string) string { var buf bytes.Buffer buf.WriteString("Erkläre die folgende DSGVO-Compliance-Bewertung für einen KI-Use-Case in verständlicher Sprache:\n\n") buf.WriteString(fmt.Sprintf("**Ergebnis:** %s\n", assessment.Feasibility)) buf.WriteString(fmt.Sprintf("**Risikostufe:** %s\n", assessment.RiskLevel)) buf.WriteString(fmt.Sprintf("**Risiko-Score:** %d/100\n", assessment.RiskScore)) buf.WriteString(fmt.Sprintf("**Komplexität:** %s\n\n", assessment.Complexity)) if len(assessment.TriggeredRules) > 0 { buf.WriteString("**Ausgelöste Regeln:**\n") for _, r := range assessment.TriggeredRules { buf.WriteString(fmt.Sprintf("- %s (%s): %s\n", r.Code, r.Severity, r.Title)) } buf.WriteString("\n") } if len(assessment.RequiredControls) > 0 { buf.WriteString("**Erforderliche Maßnahmen:**\n") for _, c := range assessment.RequiredControls { buf.WriteString(fmt.Sprintf("- %s: %s\n", c.Title, c.Description)) } buf.WriteString("\n") } if assessment.DSFARecommended { buf.WriteString("**Hinweis:** Eine Datenschutz-Folgenabschätzung (DSFA) wird empfohlen.\n\n") } if assessment.Art22Risk { buf.WriteString("**Warnung:** Es besteht ein Risiko unter Art. 22 DSGVO (automatisierte Einzelentscheidungen).\n\n") } // Include legal context from RAG if available if legalContext != "" { buf.WriteString(legalContext) } buf.WriteString("\nBitte erkläre:\n") buf.WriteString("1. Warum dieses Ergebnis zustande kam (mit Bezug auf die angegebenen Rechtsgrundlagen)\n") buf.WriteString("2. Welche konkreten Schritte unternommen werden sollten\n") buf.WriteString("3. Welche Alternativen es gibt, falls der Use Case abgelehnt wurde\n") buf.WriteString("4. Welche spezifischen Artikel aus DSGVO/AI Act beachtet werden müssen\n") return buf.String() } // ============================================================================ // GET /sdk/v1/ucca/export/:id - Export assessment // ============================================================================ // Export exports an assessment as JSON or Markdown func (h *UCCAHandlers) Export(c *gin.Context) { id, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"}) return } format := c.DefaultQuery("format", "json") assessment, err := h.store.GetAssessment(c.Request.Context(), id) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } if assessment == nil { c.JSON(http.StatusNotFound, gin.H{"error": "not found"}) return } if format == "md" { markdown := generateMarkdownExport(assessment) c.Header("Content-Type", "text/markdown; charset=utf-8") c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=ucca_assessment_%s.md", id.String()[:8])) c.Data(http.StatusOK, "text/markdown; charset=utf-8", []byte(markdown)) return } // JSON export c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=ucca_assessment_%s.json", id.String()[:8])) c.JSON(http.StatusOK, gin.H{ "exported_at": time.Now().UTC().Format(time.RFC3339), "assessment": assessment, }) } // generateMarkdownExport creates a Markdown export of the assessment func generateMarkdownExport(a *ucca.Assessment) string { var buf bytes.Buffer buf.WriteString("# UCCA Use-Case Assessment\n\n") buf.WriteString(fmt.Sprintf("**ID:** %s\n", a.ID.String())) buf.WriteString(fmt.Sprintf("**Erstellt:** %s\n", a.CreatedAt.Format("02.01.2006 15:04"))) buf.WriteString(fmt.Sprintf("**Domain:** %s\n\n", a.Domain)) buf.WriteString("## Ergebnis\n\n") buf.WriteString(fmt.Sprintf("| Kriterium | Wert |\n")) buf.WriteString("|-----------|------|\n") buf.WriteString(fmt.Sprintf("| Machbarkeit | **%s** |\n", a.Feasibility)) buf.WriteString(fmt.Sprintf("| Risikostufe | %s |\n", a.RiskLevel)) buf.WriteString(fmt.Sprintf("| Risiko-Score | %d/100 |\n", a.RiskScore)) buf.WriteString(fmt.Sprintf("| Komplexität | %s |\n", a.Complexity)) buf.WriteString(fmt.Sprintf("| DSFA empfohlen | %t |\n", a.DSFARecommended)) buf.WriteString(fmt.Sprintf("| Art. 22 Risiko | %t |\n", a.Art22Risk)) buf.WriteString(fmt.Sprintf("| Training erlaubt | %s |\n\n", a.TrainingAllowed)) if len(a.TriggeredRules) > 0 { buf.WriteString("## Ausgelöste Regeln\n\n") buf.WriteString("| Code | Titel | Schwere | Score |\n") buf.WriteString("|------|-------|---------|-------|\n") for _, r := range a.TriggeredRules { buf.WriteString(fmt.Sprintf("| %s | %s | %s | +%d |\n", r.Code, r.Title, r.Severity, r.ScoreDelta)) } buf.WriteString("\n") } if len(a.RequiredControls) > 0 { buf.WriteString("## Erforderliche Kontrollen\n\n") for _, c := range a.RequiredControls { buf.WriteString(fmt.Sprintf("### %s\n", c.Title)) buf.WriteString(fmt.Sprintf("%s\n\n", c.Description)) if c.GDPRRef != "" { buf.WriteString(fmt.Sprintf("*Referenz: %s*\n\n", c.GDPRRef)) } } } if len(a.RecommendedArchitecture) > 0 { buf.WriteString("## Empfohlene Architektur-Patterns\n\n") for _, p := range a.RecommendedArchitecture { buf.WriteString(fmt.Sprintf("### %s\n", p.Title)) buf.WriteString(fmt.Sprintf("%s\n\n", p.Description)) } } if len(a.ForbiddenPatterns) > 0 { buf.WriteString("## Verbotene Patterns\n\n") for _, p := range a.ForbiddenPatterns { buf.WriteString(fmt.Sprintf("### %s\n", p.Title)) buf.WriteString(fmt.Sprintf("**Grund:** %s\n\n", p.Reason)) } } if a.ExplanationText != nil && *a.ExplanationText != "" { buf.WriteString("## KI-Erklärung\n\n") buf.WriteString(*a.ExplanationText) buf.WriteString("\n\n") } buf.WriteString("---\n") buf.WriteString(fmt.Sprintf("*Generiert mit UCCA Policy Version %s*\n", a.PolicyVersion)) return buf.String() } // ============================================================================ // GET /sdk/v1/ucca/stats - Get statistics // ============================================================================ // GetStats returns UCCA statistics for a tenant func (h *UCCAHandlers) GetStats(c *gin.Context) { tenantID := rbac.GetTenantID(c) if tenantID == uuid.Nil { c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"}) return } stats, err := h.store.GetStats(c.Request.Context(), tenantID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) return } c.JSON(http.StatusOK, stats) } // ============================================================================ // POST /sdk/v1/ucca/wizard/ask - Legal Assistant for Wizard // ============================================================================ // WizardAskRequest represents a question to the Legal Assistant type WizardAskRequest struct { Question string `json:"question" binding:"required"` StepNumber int `json:"step_number"` FieldID string `json:"field_id,omitempty"` // Optional: Specific field context CurrentData map[string]interface{} `json:"current_data,omitempty"` // Current wizard answers } // WizardAskResponse represents the Legal Assistant response type WizardAskResponse struct { Answer string `json:"answer"` Sources []LegalSource `json:"sources,omitempty"` RelatedFields []string `json:"related_fields,omitempty"` GeneratedAt time.Time `json:"generated_at"` Model string `json:"model"` } // LegalSource represents a legal reference used in the answer type LegalSource struct { Regulation string `json:"regulation"` Article string `json:"article,omitempty"` Text string `json:"text,omitempty"` } // AskWizardQuestion handles legal questions from the wizard func (h *UCCAHandlers) AskWizardQuestion(c *gin.Context) { var req WizardAskRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Build context-aware query for Legal RAG ragQuery := buildWizardRAGQuery(req) // Search legal corpus for relevant context var legalResults []ucca.LegalSearchResult var sources []LegalSource if h.legalRAGClient != nil { results, err := h.legalRAGClient.Search(c.Request.Context(), ragQuery, nil, 5) if err != nil { // Log but continue without RAG context fmt.Printf("Warning: Legal RAG search failed: %v\n", err) } else { legalResults = results // Convert to sources sources = make([]LegalSource, len(results)) for i, r := range results { sources[i] = LegalSource{ Regulation: r.RegulationName, Article: r.Article, Text: truncateText(r.Text, 200), } } } } // Build prompt for LLM prompt := buildWizardAssistantPrompt(req, legalResults) // Build system prompt with step context systemPrompt := buildWizardSystemPrompt(req.StepNumber) // Call LLM chatReq := &llm.ChatRequest{ Messages: []llm.Message{ {Role: "system", Content: systemPrompt}, {Role: "user", Content: prompt}, }, MaxTokens: 1024, Temperature: 0.3, // Low temperature for precise legal answers } response, err := h.providerRegistry.Chat(c.Request.Context(), chatReq) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "LLM call failed: " + err.Error()}) return } // Identify related wizard fields based on question relatedFields := identifyRelatedFields(req.Question) c.JSON(http.StatusOK, WizardAskResponse{ Answer: response.Message.Content, Sources: sources, RelatedFields: relatedFields, GeneratedAt: time.Now().UTC(), Model: response.Model, }) } // buildWizardRAGQuery creates an optimized query for Legal RAG search func buildWizardRAGQuery(req WizardAskRequest) string { // Start with the user's question query := req.Question // Add context based on step number stepContext := map[int]string{ 1: "KI-Anwendung Use Case", 2: "personenbezogene Daten Datenkategorien DSGVO Art. 4 Art. 9", 3: "Verarbeitungszweck Profiling Scoring automatisierte Entscheidung Art. 22", 4: "Hosting Cloud On-Premises Auftragsverarbeitung", 5: "Standardvertragsklauseln SCC Drittlandtransfer TIA Transfer Impact Assessment Art. 44 Art. 46", 6: "KI-Modell Training RAG Finetuning", 7: "Auftragsverarbeitungsvertrag AVV DSFA Verarbeitungsverzeichnis Art. 28 Art. 30 Art. 35", 8: "Automatisierung Human-in-the-Loop Art. 22 AI Act", } if context, ok := stepContext[req.StepNumber]; ok { query = query + " " + context } return query } // buildWizardSystemPrompt creates the system prompt for the Legal Assistant func buildWizardSystemPrompt(stepNumber int) string { basePrompt := `Du bist ein freundlicher Rechtsassistent, der Nutzern hilft, datenschutzrechtliche Begriffe und Anforderungen zu verstehen. DEINE AUFGABE: - Erkläre rechtliche Begriffe in einfacher, verständlicher Sprache - Beantworte Fragen zum aktuellen Wizard-Schritt - Hilf dem Nutzer, die richtigen Antworten im Wizard zu geben - Verweise auf relevante Rechtsquellen (DSGVO-Artikel, etc.) WICHTIGE REGELN: - Antworte IMMER auf Deutsch - Verwende einfache Sprache, keine Juristensprache - Gib konkrete Beispiele wenn möglich - Bei Unsicherheit empfehle die Rücksprache mit einem Datenschutzbeauftragten - Du darfst KEINE Rechtsberatung geben, nur erklären ANTWORT-FORMAT: - Kurz und prägnant (max. 3-4 Sätze für einfache Fragen) - Strukturiert mit Aufzählungen bei komplexen Themen - Immer mit Quellenangabe am Ende (z.B. "Siehe: DSGVO Art. 9")` // Add step-specific context stepContexts := map[int]string{ 1: "\n\nKONTEXT: Der Nutzer befindet sich im ersten Schritt und gibt grundlegende Informationen zum KI-Vorhaben ein.", 2: "\n\nKONTEXT: Der Nutzer gibt an, welche Datenarten verarbeitet werden. Erkläre die Unterschiede zwischen personenbezogenen Daten, Art. 9 Daten (besondere Kategorien), biometrischen Daten, etc.", 3: "\n\nKONTEXT: Der Nutzer gibt den Verarbeitungszweck an. Erkläre Begriffe wie Profiling, Scoring, systematische Überwachung, automatisierte Entscheidungen mit rechtlicher Wirkung.", 4: "\n\nKONTEXT: Der Nutzer gibt Hosting-Informationen an. Erkläre Cloud vs. On-Premises, wann Drittlandtransfer vorliegt, Unterschiede zwischen EU/EWR und Drittländern.", 5: "\n\nKONTEXT: Der Nutzer beantwortet Fragen zu SCC und TIA. Erkläre Standardvertragsklauseln (SCC), Transfer Impact Assessment (TIA), das Data Privacy Framework (DPF), und wann welche Instrumente erforderlich sind.", 6: "\n\nKONTEXT: Der Nutzer gibt KI-Modell-Informationen an. Erkläre RAG vs. Training/Finetuning, warum Training mit personenbezogenen Daten problematisch ist, und welche Opt-Out-Klauseln wichtig sind.", 7: "\n\nKONTEXT: Der Nutzer beantwortet Fragen zu Verträgen. Erkläre den Auftragsverarbeitungsvertrag (AVV), die Datenschutz-Folgenabschätzung (DSFA), das Verarbeitungsverzeichnis (VVT), und wann diese erforderlich sind.", 8: "\n\nKONTEXT: Der Nutzer gibt den Automatisierungsgrad an. Erkläre Human-in-the-Loop, Art. 22 DSGVO (automatisierte Einzelentscheidungen), und die Anforderungen des AI Acts.", } if context, ok := stepContexts[stepNumber]; ok { basePrompt += context } return basePrompt } // buildWizardAssistantPrompt creates the user prompt with legal context func buildWizardAssistantPrompt(req WizardAskRequest, legalResults []ucca.LegalSearchResult) string { var buf bytes.Buffer buf.WriteString(fmt.Sprintf("FRAGE DES NUTZERS:\n%s\n\n", req.Question)) // Add legal context if available if len(legalResults) > 0 { buf.WriteString("RELEVANTE RECHTSGRUNDLAGEN (aus unserer Bibliothek):\n\n") for i, result := range legalResults { buf.WriteString(fmt.Sprintf("%d. %s", i+1, result.RegulationName)) if result.Article != "" { buf.WriteString(fmt.Sprintf(" - Art. %s", result.Article)) if result.Paragraph != "" { buf.WriteString(fmt.Sprintf(" Abs. %s", result.Paragraph)) } } buf.WriteString("\n") buf.WriteString(fmt.Sprintf(" %s\n\n", truncateText(result.Text, 300))) } } // Add field context if provided if req.FieldID != "" { buf.WriteString(fmt.Sprintf("AKTUELLES FELD: %s\n\n", req.FieldID)) } buf.WriteString("Bitte beantworte die Frage kurz und verständlich. Verwende die angegebenen Rechtsgrundlagen als Referenz.") return buf.String() } // identifyRelatedFields identifies wizard fields related to the question func identifyRelatedFields(question string) []string { question = strings.ToLower(question) var related []string // Map keywords to wizard field IDs keywordMapping := map[string][]string{ "personenbezogen": {"data_types.personal_data"}, "art. 9": {"data_types.article_9_data"}, "sensibel": {"data_types.article_9_data"}, "gesundheit": {"data_types.article_9_data"}, "minderjährig": {"data_types.minor_data"}, "kinder": {"data_types.minor_data"}, "biometrisch": {"data_types.biometric_data"}, "gesicht": {"data_types.biometric_data"}, "kennzeichen": {"data_types.license_plates"}, "standort": {"data_types.location_data"}, "gps": {"data_types.location_data"}, "profiling": {"purpose.profiling"}, "scoring": {"purpose.evaluation_scoring"}, "überwachung": {"processing.systematic_monitoring"}, "automatisch": {"outputs.decision_with_legal_effect", "automation"}, "entscheidung": {"outputs.decision_with_legal_effect"}, "cloud": {"hosting.type", "hosting.region"}, "on-premises": {"hosting.type"}, "lokal": {"hosting.type"}, "scc": {"contracts.scc.present", "contracts.scc.version"}, "standardvertrags": {"contracts.scc.present"}, "drittland": {"hosting.region", "provider.location"}, "usa": {"hosting.region", "provider.location", "provider.dpf_certified"}, "transfer": {"hosting.region", "contracts.tia.present"}, "tia": {"contracts.tia.present", "contracts.tia.result"}, "dpf": {"provider.dpf_certified"}, "data privacy": {"provider.dpf_certified"}, "avv": {"contracts.avv.present"}, "auftragsverarbeitung": {"contracts.avv.present"}, "dsfa": {"governance.dsfa_completed"}, "folgenabschätzung": {"governance.dsfa_completed"}, "verarbeitungsverzeichnis": {"governance.vvt_entry"}, "training": {"model_usage.training", "provider.uses_data_for_training"}, "finetuning": {"model_usage.training"}, "rag": {"model_usage.rag"}, "human": {"processing.human_oversight"}, "aufsicht": {"processing.human_oversight"}, } seen := make(map[string]bool) for keyword, fields := range keywordMapping { if strings.Contains(question, keyword) { for _, field := range fields { if !seen[field] { related = append(related, field) seen[field] = true } } } } return related } // ============================================================================ // GET /sdk/v1/ucca/wizard/schema - Get Wizard Schema // ============================================================================ // GetWizardSchema returns the wizard schema for the frontend func (h *UCCAHandlers) GetWizardSchema(c *gin.Context) { // For now, return a static schema info // In future, this could be loaded from the YAML file c.JSON(http.StatusOK, gin.H{ "version": "1.1", "total_steps": 8, "default_mode": "simple", "legal_assistant": gin.H{ "enabled": true, "endpoint": "/sdk/v1/ucca/wizard/ask", "max_tokens": 1024, "example_questions": []string{ "Was sind personenbezogene Daten?", "Was ist der Unterschied zwischen AVV und SCC?", "Brauche ich ein TIA?", "Was bedeutet Profiling?", "Was ist Art. 9 DSGVO?", "Wann brauche ich eine DSFA?", "Was ist das Data Privacy Framework?", }, }, "steps": []gin.H{ {"number": 1, "title": "Grundlegende Informationen", "icon": "info"}, {"number": 2, "title": "Welche Daten werden verarbeitet?", "icon": "database"}, {"number": 3, "title": "Wofür wird die KI eingesetzt?", "icon": "target"}, {"number": 4, "title": "Wo läuft die KI?", "icon": "server"}, {"number": 5, "title": "Internationaler Datentransfer", "icon": "globe"}, {"number": 6, "title": "KI-Modell und Training", "icon": "brain"}, {"number": 7, "title": "Verträge & Compliance", "icon": "file-contract"}, {"number": 8, "title": "Automatisierung & Kontrolle", "icon": "user-check"}, }, }) } // ============================================================================ // Helper functions // ============================================================================ // truncateText truncates a string to maxLen characters func truncateText(text string, maxLen int) string { if len(text) <= maxLen { return text } return text[:maxLen] + "..." }