package handlers import ( "bytes" "fmt" "net/http" "time" "github.com/breakpilot/ai-compliance-sdk/internal/llm" "github.com/breakpilot/ai-compliance-sdk/internal/ucca" "github.com/gin-gonic/gin" "github.com/google/uuid" ) // 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 { req.Language = "de" } if req.Language == "" { req.Language = "de" } 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 { fmt.Printf("Warning: Could not get legal context: %v\n", err) } else { legalContextStr = h.legalRAGClient.FormatLegalContextForPrompt(legalContext) } } prompt := buildExplanationPrompt(assessment, req.Language, legalContextStr) 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 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 _, ctrl := range assessment.RequiredControls { buf.WriteString(fmt.Sprintf("- %s: %s\n", ctrl.Title, ctrl.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") } 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() } // 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 } 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("| 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 _, ctrl := range a.RequiredControls { buf.WriteString(fmt.Sprintf("### %s\n", ctrl.Title)) buf.WriteString(fmt.Sprintf("%s\n\n", ctrl.Description)) if ctrl.GDPRRef != "" { buf.WriteString(fmt.Sprintf("*Referenz: %s*\n\n", ctrl.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() } // truncateText truncates a string to maxLen characters func truncateText(text string, maxLen int) string { if len(text) <= maxLen { return text } return text[:maxLen] + "..." }