This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/ai-compliance-sdk/internal/api/handlers/ucca_handlers.go
Benjamin Admin 21a844cb8a fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.

This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).

Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 09:51:32 +01:00

1056 lines
36 KiB
Go

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