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>
281 lines
11 KiB
Go
281 lines
11 KiB
Go
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
|
|
}
|