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>
This commit is contained in:
File diff suppressed because it is too large
Load Diff
203
ai-compliance-sdk/internal/api/handlers/ucca_handlers_catalog.go
Normal file
203
ai-compliance-sdk/internal/api/handlers/ucca_handlers_catalog.go
Normal file
@@ -0,0 +1,203 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/ucca"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ListPatterns returns all available architecture patterns
|
||||
func (h *UCCAHandlers) ListPatterns(c *gin.Context) {
|
||||
var response []gin.H
|
||||
|
||||
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 {
|
||||
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})
|
||||
}
|
||||
|
||||
// ListControls returns all available compliance controls
|
||||
func (h *UCCAHandlers) ListControls(c *gin.Context) {
|
||||
var response []gin.H
|
||||
|
||||
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 {
|
||||
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})
|
||||
}
|
||||
|
||||
// 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})
|
||||
}
|
||||
|
||||
// ListExamples returns all available didactic examples
|
||||
func (h *UCCAHandlers) ListExamples(c *gin.Context) {
|
||||
examples := ucca.GetAllExamples()
|
||||
|
||||
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})
|
||||
}
|
||||
|
||||
// ListRules returns all rules for transparency
|
||||
func (h *UCCAHandlers) ListRules(c *gin.Context) {
|
||||
var response []gin.H
|
||||
var policyVersion string
|
||||
|
||||
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 {
|
||||
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",
|
||||
},
|
||||
})
|
||||
}
|
||||
243
ai-compliance-sdk/internal/api/handlers/ucca_handlers_explain.go
Normal file
243
ai-compliance-sdk/internal/api/handlers/ucca_handlers_explain.go
Normal file
@@ -0,0 +1,243 @@
|
||||
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] + "..."
|
||||
}
|
||||
280
ai-compliance-sdk/internal/api/handlers/ucca_handlers_wizard.go
Normal file
280
ai-compliance-sdk/internal/api/handlers/ucca_handlers_wizard.go
Normal file
@@ -0,0 +1,280 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/llm"
|
||||
"github.com/breakpilot/ai-compliance-sdk/internal/ucca"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 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"`
|
||||
CurrentData map[string]interface{} `json:"current_data,omitempty"`
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
ragQuery := buildWizardRAGQuery(req)
|
||||
|
||||
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 {
|
||||
fmt.Printf("Warning: Legal RAG search failed: %v\n", err)
|
||||
} else {
|
||||
legalResults = results
|
||||
sources = make([]LegalSource, len(results))
|
||||
for i, r := range results {
|
||||
sources[i] = LegalSource{
|
||||
Regulation: r.RegulationName,
|
||||
Article: r.Article,
|
||||
Text: truncateText(r.Text, 200),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
prompt := buildWizardAssistantPrompt(req, legalResults)
|
||||
systemPrompt := buildWizardSystemPrompt(req.StepNumber)
|
||||
|
||||
chatReq := &llm.ChatRequest{
|
||||
Messages: []llm.Message{
|
||||
{Role: "system", Content: systemPrompt},
|
||||
{Role: "user", Content: prompt},
|
||||
},
|
||||
MaxTokens: 1024,
|
||||
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
|
||||
}
|
||||
|
||||
relatedFields := identifyRelatedFields(req.Question)
|
||||
|
||||
c.JSON(http.StatusOK, WizardAskResponse{
|
||||
Answer: response.Message.Content,
|
||||
Sources: sources,
|
||||
RelatedFields: relatedFields,
|
||||
GeneratedAt: time.Now().UTC(),
|
||||
Model: response.Model,
|
||||
})
|
||||
}
|
||||
|
||||
// GetWizardSchema returns the wizard schema for the frontend
|
||||
func (h *UCCAHandlers) GetWizardSchema(c *gin.Context) {
|
||||
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"},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// buildWizardRAGQuery creates an optimized query for Legal RAG search
|
||||
func buildWizardRAGQuery(req WizardAskRequest) string {
|
||||
query := req.Question
|
||||
|
||||
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")`
|
||||
|
||||
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))
|
||||
|
||||
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)))
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user