Files
breakpilot-compliance/ai-compliance-sdk/internal/api/handlers/ucca_handlers_explain.go
Sharang Parnerkar 9f96061631 refactor(go): split training/store, ucca/rules, ucca_handlers, document_export under 500 LOC
Each of the four oversized files (training/store.go 1569 LOC, ucca/rules.go 1231 LOC,
ucca_handlers.go 1135 LOC, document_export.go 1101 LOC) is split by logical group
into same-package files, all under the 500-line hard cap. Zero behavior changes,
no renamed exported symbols. Also fixed pre-existing hazard_library split (missing
functions and duplicate UUID keys from a prior session).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 09:29:54 +02:00

244 lines
8.0 KiB
Go

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] + "..."
}