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>
244 lines
8.0 KiB
Go
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] + "..."
|
|
}
|