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