feat: AI Act Decision Tree — Zwei-Achsen-Klassifikation (GPAI + High-Risk)
Interaktiver 12-Fragen-Entscheidungsbaum für die AI Act Klassifikation auf zwei Achsen: High-Risk (Anhang III, Q1-Q7) und GPAI (Art. 51-56, Q8-Q12). Deterministische Auswertung ohne LLM. Backend (Go): - Neue Structs: GPAIClassification, DecisionTreeAnswer, DecisionTreeResult - Decision Tree Engine mit BuildDecisionTreeDefinition() und EvaluateDecisionTree() - Store-Methoden für CRUD der Ergebnisse - API-Endpoints: GET/POST /decision-tree, GET/DELETE /decision-tree/results - 12 Unit Tests (alle bestanden) Frontend (Next.js): - DecisionTreeWizard: Wizard-UI mit Ja/Nein-Fragen, Dual-Progress-Bar, Ergebnis-Ansicht - AI Act Page refactored: Tabs (Übersicht | Entscheidungsbaum | Ergebnisse) - Proxy-Route für decision-tree Endpoints Migration 083: ai_act_decision_tree_results Tabelle Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -270,6 +270,16 @@ func main() {
|
||||
uccaRoutes.POST("/escalations/:id/review", escalationHandlers.StartReview)
|
||||
uccaRoutes.POST("/escalations/:id/decide", escalationHandlers.DecideEscalation)
|
||||
|
||||
// AI Act Decision Tree
|
||||
dtRoutes := uccaRoutes.Group("/decision-tree")
|
||||
{
|
||||
dtRoutes.GET("", uccaHandlers.GetDecisionTree)
|
||||
dtRoutes.POST("/evaluate", uccaHandlers.EvaluateDecisionTree)
|
||||
dtRoutes.GET("/results", uccaHandlers.ListDecisionTreeResults)
|
||||
dtRoutes.GET("/results/:id", uccaHandlers.GetDecisionTreeResult)
|
||||
dtRoutes.DELETE("/results/:id", uccaHandlers.DeleteDecisionTreeResult)
|
||||
}
|
||||
|
||||
// Obligations framework (v2 with TOM mapping)
|
||||
obligationsHandlers.RegisterRoutes(uccaRoutes)
|
||||
}
|
||||
|
||||
@@ -1122,6 +1122,114 @@ func (h *UCCAHandlers) GetWizardSchema(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// AI Act Decision Tree Endpoints
|
||||
// ============================================================================
|
||||
|
||||
// GetDecisionTree returns the decision tree structure for the frontend
|
||||
// GET /sdk/v1/ucca/decision-tree
|
||||
func (h *UCCAHandlers) GetDecisionTree(c *gin.Context) {
|
||||
tree := ucca.BuildDecisionTreeDefinition()
|
||||
c.JSON(http.StatusOK, tree)
|
||||
}
|
||||
|
||||
// EvaluateDecisionTree evaluates the decision tree answers and stores the result
|
||||
// POST /sdk/v1/ucca/decision-tree/evaluate
|
||||
func (h *UCCAHandlers) EvaluateDecisionTree(c *gin.Context) {
|
||||
tenantID := rbac.GetTenantID(c)
|
||||
if tenantID == uuid.Nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
|
||||
return
|
||||
}
|
||||
|
||||
var req ucca.DecisionTreeEvalRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if req.SystemName == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "system_name is required"})
|
||||
return
|
||||
}
|
||||
|
||||
// Evaluate
|
||||
result := ucca.EvaluateDecisionTree(&req)
|
||||
result.TenantID = tenantID
|
||||
|
||||
// Parse optional project_id
|
||||
if projectIDStr := c.Query("project_id"); projectIDStr != "" {
|
||||
if pid, err := uuid.Parse(projectIDStr); err == nil {
|
||||
result.ProjectID = &pid
|
||||
}
|
||||
}
|
||||
|
||||
// Store result
|
||||
if err := h.store.CreateDecisionTreeResult(c.Request.Context(), result); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, result)
|
||||
}
|
||||
|
||||
// ListDecisionTreeResults returns stored decision tree results for a tenant
|
||||
// GET /sdk/v1/ucca/decision-tree/results
|
||||
func (h *UCCAHandlers) ListDecisionTreeResults(c *gin.Context) {
|
||||
tenantID := rbac.GetTenantID(c)
|
||||
if tenantID == uuid.Nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "tenant ID required"})
|
||||
return
|
||||
}
|
||||
|
||||
results, err := h.store.ListDecisionTreeResults(c.Request.Context(), tenantID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"results": results, "total": len(results)})
|
||||
}
|
||||
|
||||
// GetDecisionTreeResult returns a single decision tree result by ID
|
||||
// GET /sdk/v1/ucca/decision-tree/results/:id
|
||||
func (h *UCCAHandlers) GetDecisionTreeResult(c *gin.Context) {
|
||||
id, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid ID"})
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.store.GetDecisionTreeResult(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if result == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// DeleteDecisionTreeResult deletes a decision tree result
|
||||
// DELETE /sdk/v1/ucca/decision-tree/results/:id
|
||||
func (h *UCCAHandlers) DeleteDecisionTreeResult(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.DeleteDecisionTreeResult(c.Request.Context(), id); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "deleted"})
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Helper functions
|
||||
// ============================================================================
|
||||
|
||||
325
ai-compliance-sdk/internal/ucca/decision_tree_engine.go
Normal file
325
ai-compliance-sdk/internal/ucca/decision_tree_engine.go
Normal file
@@ -0,0 +1,325 @@
|
||||
package ucca
|
||||
|
||||
// ============================================================================
|
||||
// AI Act Decision Tree Engine
|
||||
// ============================================================================
|
||||
//
|
||||
// Two-axis classification:
|
||||
// Axis 1 (Q1–Q7): High-Risk classification based on Annex III
|
||||
// Axis 2 (Q8–Q12): GPAI classification based on Art. 51–56
|
||||
//
|
||||
// Deterministic evaluation — no LLM involved.
|
||||
//
|
||||
// ============================================================================
|
||||
|
||||
// Question IDs
|
||||
const (
|
||||
Q1 = "Q1" // Uses AI?
|
||||
Q2 = "Q2" // Biometric identification?
|
||||
Q3 = "Q3" // Critical infrastructure?
|
||||
Q4 = "Q4" // Education / employment / HR?
|
||||
Q5 = "Q5" // Essential services (credit, insurance)?
|
||||
Q6 = "Q6" // Law enforcement / migration / justice?
|
||||
Q7 = "Q7" // Autonomous decisions with legal effect?
|
||||
Q8 = "Q8" // Foundation Model / GPAI?
|
||||
Q9 = "Q9" // Generates content (text, image, code, audio)?
|
||||
Q10 = "Q10" // Trained with >10^25 FLOP?
|
||||
Q11 = "Q11" // Model provided as API/service for third parties?
|
||||
Q12 = "Q12" // Significant EU market penetration?
|
||||
)
|
||||
|
||||
// BuildDecisionTreeDefinition returns the full decision tree structure for the frontend
|
||||
func BuildDecisionTreeDefinition() *DecisionTreeDefinition {
|
||||
return &DecisionTreeDefinition{
|
||||
ID: "ai_act_two_axis",
|
||||
Name: "AI Act Zwei-Achsen-Klassifikation",
|
||||
Version: "1.0.0",
|
||||
Questions: []DecisionTreeQuestion{
|
||||
// === Axis 1: High-Risk (Annex III) ===
|
||||
{
|
||||
ID: Q1,
|
||||
Axis: "high_risk",
|
||||
Question: "Setzt Ihr System KI-Technologie ein?",
|
||||
Description: "KI im Sinne des AI Act umfasst maschinelles Lernen, logik- und wissensbasierte Ansätze sowie statistische Methoden, die für eine gegebene Reihe von Zielen Ergebnisse wie Inhalte, Vorhersagen, Empfehlungen oder Entscheidungen erzeugen.",
|
||||
ArticleRef: "Art. 3 Nr. 1",
|
||||
},
|
||||
{
|
||||
ID: Q2,
|
||||
Axis: "high_risk",
|
||||
Question: "Wird das System für biometrische Identifikation oder Kategorisierung natürlicher Personen verwendet?",
|
||||
Description: "Dazu zählen Gesichtserkennung, Stimmerkennung, Fingerabdruck-Analyse, Gangerkennung oder andere biometrische Merkmale zur Identifikation oder Kategorisierung.",
|
||||
ArticleRef: "Anhang III Nr. 1",
|
||||
SkipIf: Q1,
|
||||
},
|
||||
{
|
||||
ID: Q3,
|
||||
Axis: "high_risk",
|
||||
Question: "Wird das System in kritischer Infrastruktur eingesetzt (Energie, Verkehr, Wasser, digitale Infrastruktur)?",
|
||||
Description: "Betrifft KI-Systeme als Sicherheitskomponenten in der Verwaltung und dem Betrieb kritischer digitaler Infrastruktur, des Straßenverkehrs oder der Wasser-, Gas-, Heizungs- oder Stromversorgung.",
|
||||
ArticleRef: "Anhang III Nr. 2",
|
||||
SkipIf: Q1,
|
||||
},
|
||||
{
|
||||
ID: Q4,
|
||||
Axis: "high_risk",
|
||||
Question: "Betrifft das System Bildung, Beschäftigung oder Personalmanagement?",
|
||||
Description: "KI zur Festlegung des Zugangs zu Bildungseinrichtungen, Bewertung von Prüfungsleistungen, Bewerbungsauswahl, Beförderungsentscheidungen oder Überwachung von Arbeitnehmern.",
|
||||
ArticleRef: "Anhang III Nr. 3–4",
|
||||
SkipIf: Q1,
|
||||
},
|
||||
{
|
||||
ID: Q5,
|
||||
Axis: "high_risk",
|
||||
Question: "Betrifft das System den Zugang zu wesentlichen Diensten (Kreditvergabe, Versicherung, öffentliche Leistungen)?",
|
||||
Description: "KI zur Bonitätsbewertung, Risikobewertung bei Versicherungen, Bewertung der Anspruchsberechtigung für öffentliche Unterstützungsleistungen oder Notdienste.",
|
||||
ArticleRef: "Anhang III Nr. 5",
|
||||
SkipIf: Q1,
|
||||
},
|
||||
{
|
||||
ID: Q6,
|
||||
Axis: "high_risk",
|
||||
Question: "Wird das System in Strafverfolgung, Migration, Asyl oder Justiz eingesetzt?",
|
||||
Description: "KI für Lügendetektoren, Beweisbewertung, Rückfallprognose, Asylentscheidungen, Grenzkontrolle, Risikobewertung bei Migration oder Unterstützung der Rechtspflege.",
|
||||
ArticleRef: "Anhang III Nr. 6–8",
|
||||
SkipIf: Q1,
|
||||
},
|
||||
{
|
||||
ID: Q7,
|
||||
Axis: "high_risk",
|
||||
Question: "Trifft das System autonome Entscheidungen mit rechtlicher Wirkung für natürliche Personen?",
|
||||
Description: "Entscheidungen, die Rechtsverhältnisse begründen, ändern oder aufheben, z.B. Kreditablehnungen, Kündigungen, Sozialleistungsentscheidungen — ohne menschliche Überprüfung im Einzelfall.",
|
||||
ArticleRef: "Art. 22 DSGVO / Art. 14 AI Act",
|
||||
SkipIf: Q1,
|
||||
},
|
||||
|
||||
// === Axis 2: GPAI (Art. 51–56) ===
|
||||
{
|
||||
ID: Q8,
|
||||
Axis: "gpai",
|
||||
Question: "Handelt es sich um ein Foundation Model oder General-Purpose AI (GPAI)?",
|
||||
Description: "Ein GPAI-Modell ist ein KI-Modell mit erheblicher Allgemeinheit, das kompetent eine breite Palette unterschiedlicher Aufgaben erfüllen kann, z.B. GPT, Claude, LLaMA, Gemini, Stable Diffusion.",
|
||||
ArticleRef: "Art. 3 Nr. 63 / Art. 51",
|
||||
},
|
||||
{
|
||||
ID: Q9,
|
||||
Axis: "gpai",
|
||||
Question: "Kann das System Inhalte generieren (Text, Bild, Code, Audio, Video)?",
|
||||
Description: "Generative KI erzeugt neue Inhalte auf Basis von Eingaben — dazu zählen Chatbots, Bild-/Videogeneratoren, Code-Assistenten, Sprachsynthese und ähnliche Systeme.",
|
||||
ArticleRef: "Art. 50 / Art. 52",
|
||||
SkipIf: Q8,
|
||||
},
|
||||
{
|
||||
ID: Q10,
|
||||
Axis: "gpai",
|
||||
Question: "Wurde das Modell mit mehr als 10²⁵ FLOP trainiert oder hat es gleichwertige Fähigkeiten?",
|
||||
Description: "GPAI-Modelle mit einem kumulativen Rechenaufwand von mehr als 10²⁵ Gleitkommaoperationen gelten als Modelle mit systemischem Risiko (Art. 51 Abs. 2).",
|
||||
ArticleRef: "Art. 51 Abs. 2",
|
||||
SkipIf: Q8,
|
||||
},
|
||||
{
|
||||
ID: Q11,
|
||||
Axis: "gpai",
|
||||
Question: "Wird das Modell als API oder Service für Dritte bereitgestellt?",
|
||||
Description: "Stellen Sie das Modell anderen Unternehmen oder Entwicklern zur Nutzung bereit (API, SaaS, Plattform-Integration)?",
|
||||
ArticleRef: "Art. 53",
|
||||
SkipIf: Q8,
|
||||
},
|
||||
{
|
||||
ID: Q12,
|
||||
Axis: "gpai",
|
||||
Question: "Hat das Modell eine signifikante Marktdurchdringung in der EU (>10.000 registrierte Geschäftsnutzer)?",
|
||||
Description: "Modelle mit hoher Marktdurchdringung können auch ohne 10²⁵ FLOP als systemisches Risiko eingestuft werden, wenn die EU-Kommission dies feststellt.",
|
||||
ArticleRef: "Art. 51 Abs. 3",
|
||||
SkipIf: Q8,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// EvaluateDecisionTree evaluates the answers and returns the combined result
|
||||
func EvaluateDecisionTree(req *DecisionTreeEvalRequest) *DecisionTreeResult {
|
||||
result := &DecisionTreeResult{
|
||||
SystemName: req.SystemName,
|
||||
SystemDescription: req.SystemDescription,
|
||||
Answers: req.Answers,
|
||||
}
|
||||
|
||||
// Evaluate Axis 1: High-Risk
|
||||
result.HighRiskResult = evaluateHighRiskAxis(req.Answers)
|
||||
|
||||
// Evaluate Axis 2: GPAI
|
||||
result.GPAIResult = evaluateGPAIAxis(req.Answers)
|
||||
|
||||
// Combine obligations and articles
|
||||
result.CombinedObligations = combineObligations(result.HighRiskResult, result.GPAIResult)
|
||||
result.ApplicableArticles = combineArticles(result.HighRiskResult, result.GPAIResult)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// evaluateHighRiskAxis determines the AI Act risk level from Q1–Q7
|
||||
func evaluateHighRiskAxis(answers map[string]DecisionTreeAnswer) AIActRiskLevel {
|
||||
// Q1: Uses AI at all?
|
||||
if !answerIsYes(answers, Q1) {
|
||||
return AIActNotApplicable
|
||||
}
|
||||
|
||||
// Q2–Q6: Annex III high-risk categories
|
||||
if answerIsYes(answers, Q2) || answerIsYes(answers, Q3) ||
|
||||
answerIsYes(answers, Q4) || answerIsYes(answers, Q5) ||
|
||||
answerIsYes(answers, Q6) {
|
||||
return AIActHighRisk
|
||||
}
|
||||
|
||||
// Q7: Autonomous decisions with legal effect
|
||||
if answerIsYes(answers, Q7) {
|
||||
return AIActHighRisk
|
||||
}
|
||||
|
||||
// AI is used but no high-risk category triggered
|
||||
return AIActMinimalRisk
|
||||
}
|
||||
|
||||
// evaluateGPAIAxis determines the GPAI classification from Q8–Q12
|
||||
func evaluateGPAIAxis(answers map[string]DecisionTreeAnswer) GPAIClassification {
|
||||
gpai := GPAIClassification{
|
||||
Category: GPAICategoryNone,
|
||||
ApplicableArticles: []string{},
|
||||
Obligations: []string{},
|
||||
}
|
||||
|
||||
// Q8: Is GPAI?
|
||||
if !answerIsYes(answers, Q8) {
|
||||
return gpai
|
||||
}
|
||||
|
||||
gpai.IsGPAI = true
|
||||
gpai.Category = GPAICategoryStandard
|
||||
gpai.ApplicableArticles = append(gpai.ApplicableArticles, "Art. 51", "Art. 53")
|
||||
gpai.Obligations = append(gpai.Obligations,
|
||||
"Technische Dokumentation erstellen (Art. 53 Abs. 1a)",
|
||||
"Informationen für nachgelagerte Anbieter bereitstellen (Art. 53 Abs. 1b)",
|
||||
"Urheberrechtsrichtlinie einhalten (Art. 53 Abs. 1c)",
|
||||
"Trainingsdaten-Zusammenfassung veröffentlichen (Art. 53 Abs. 1d)",
|
||||
)
|
||||
|
||||
// Q9: Generative AI — adds transparency obligations
|
||||
if answerIsYes(answers, Q9) {
|
||||
gpai.ApplicableArticles = append(gpai.ApplicableArticles, "Art. 50")
|
||||
gpai.Obligations = append(gpai.Obligations,
|
||||
"KI-generierte Inhalte kennzeichnen (Art. 50 Abs. 2)",
|
||||
"Maschinenlesbare Kennzeichnung synthetischer Inhalte (Art. 50 Abs. 2)",
|
||||
)
|
||||
}
|
||||
|
||||
// Q10: Systemic risk threshold (>10^25 FLOP)
|
||||
if answerIsYes(answers, Q10) {
|
||||
gpai.IsSystemicRisk = true
|
||||
gpai.Category = GPAICategorySystemic
|
||||
gpai.ApplicableArticles = append(gpai.ApplicableArticles, "Art. 55")
|
||||
gpai.Obligations = append(gpai.Obligations,
|
||||
"Modellbewertung nach Stand der Technik durchführen (Art. 55 Abs. 1a)",
|
||||
"Systemische Risiken bewerten und mindern (Art. 55 Abs. 1b)",
|
||||
"Schwerwiegende Vorfälle melden (Art. 55 Abs. 1c)",
|
||||
"Angemessenes Cybersicherheitsniveau gewährleisten (Art. 55 Abs. 1d)",
|
||||
)
|
||||
}
|
||||
|
||||
// Q11: API/Service provider — additional downstream obligations
|
||||
if answerIsYes(answers, Q11) {
|
||||
gpai.Obligations = append(gpai.Obligations,
|
||||
"Downstream-Informationspflichten erfüllen (Art. 53 Abs. 1b)",
|
||||
)
|
||||
}
|
||||
|
||||
// Q12: Significant market penetration — potential systemic risk
|
||||
if answerIsYes(answers, Q12) && !gpai.IsSystemicRisk {
|
||||
// EU Commission can designate as systemic risk
|
||||
gpai.ApplicableArticles = append(gpai.ApplicableArticles, "Art. 51 Abs. 3")
|
||||
gpai.Obligations = append(gpai.Obligations,
|
||||
"Achtung: EU-Kommission kann GPAI mit hoher Marktdurchdringung als systemisches Risiko einstufen (Art. 51 Abs. 3)",
|
||||
)
|
||||
}
|
||||
|
||||
return gpai
|
||||
}
|
||||
|
||||
// combineObligations merges obligations from both axes
|
||||
func combineObligations(highRisk AIActRiskLevel, gpai GPAIClassification) []string {
|
||||
var obligations []string
|
||||
|
||||
// High-Risk obligations
|
||||
switch highRisk {
|
||||
case AIActHighRisk:
|
||||
obligations = append(obligations,
|
||||
"Risikomanagementsystem einrichten (Art. 9)",
|
||||
"Daten-Governance sicherstellen (Art. 10)",
|
||||
"Technische Dokumentation erstellen (Art. 11)",
|
||||
"Protokollierungsfunktion implementieren (Art. 12)",
|
||||
"Transparenz und Nutzerinformation (Art. 13)",
|
||||
"Menschliche Aufsicht ermöglichen (Art. 14)",
|
||||
"Genauigkeit, Robustheit und Cybersicherheit (Art. 15)",
|
||||
"EU-Datenbank-Registrierung (Art. 49)",
|
||||
)
|
||||
case AIActMinimalRisk:
|
||||
obligations = append(obligations,
|
||||
"Freiwillige Verhaltenskodizes empfohlen (Art. 95)",
|
||||
)
|
||||
case AIActNotApplicable:
|
||||
// No obligations
|
||||
}
|
||||
|
||||
// GPAI obligations
|
||||
obligations = append(obligations, gpai.Obligations...)
|
||||
|
||||
// Universal obligation for all AI users
|
||||
if highRisk != AIActNotApplicable {
|
||||
obligations = append(obligations,
|
||||
"KI-Kompetenz sicherstellen (Art. 4)",
|
||||
"Verbotene Praktiken vermeiden (Art. 5)",
|
||||
)
|
||||
}
|
||||
|
||||
return obligations
|
||||
}
|
||||
|
||||
// combineArticles merges applicable articles from both axes
|
||||
func combineArticles(highRisk AIActRiskLevel, gpai GPAIClassification) []string {
|
||||
articles := map[string]bool{}
|
||||
|
||||
// Universal
|
||||
if highRisk != AIActNotApplicable {
|
||||
articles["Art. 4"] = true
|
||||
articles["Art. 5"] = true
|
||||
}
|
||||
|
||||
// High-Risk
|
||||
switch highRisk {
|
||||
case AIActHighRisk:
|
||||
for _, a := range []string{"Art. 9", "Art. 10", "Art. 11", "Art. 12", "Art. 13", "Art. 14", "Art. 15", "Art. 26", "Art. 49"} {
|
||||
articles[a] = true
|
||||
}
|
||||
case AIActMinimalRisk:
|
||||
articles["Art. 95"] = true
|
||||
}
|
||||
|
||||
// GPAI
|
||||
for _, a := range gpai.ApplicableArticles {
|
||||
articles[a] = true
|
||||
}
|
||||
|
||||
var result []string
|
||||
for a := range articles {
|
||||
result = append(result, a)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// answerIsYes checks if a question was answered with "yes" (true)
|
||||
func answerIsYes(answers map[string]DecisionTreeAnswer, questionID string) bool {
|
||||
a, ok := answers[questionID]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return a.Value
|
||||
}
|
||||
420
ai-compliance-sdk/internal/ucca/decision_tree_engine_test.go
Normal file
420
ai-compliance-sdk/internal/ucca/decision_tree_engine_test.go
Normal file
@@ -0,0 +1,420 @@
|
||||
package ucca
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBuildDecisionTreeDefinition_ReturnsValidTree(t *testing.T) {
|
||||
tree := BuildDecisionTreeDefinition()
|
||||
|
||||
if tree == nil {
|
||||
t.Fatal("Expected non-nil tree definition")
|
||||
}
|
||||
if tree.ID != "ai_act_two_axis" {
|
||||
t.Errorf("Expected ID 'ai_act_two_axis', got '%s'", tree.ID)
|
||||
}
|
||||
if tree.Version != "1.0.0" {
|
||||
t.Errorf("Expected version '1.0.0', got '%s'", tree.Version)
|
||||
}
|
||||
if len(tree.Questions) != 12 {
|
||||
t.Errorf("Expected 12 questions, got %d", len(tree.Questions))
|
||||
}
|
||||
|
||||
// Check axis distribution
|
||||
hrCount := 0
|
||||
gpaiCount := 0
|
||||
for _, q := range tree.Questions {
|
||||
switch q.Axis {
|
||||
case "high_risk":
|
||||
hrCount++
|
||||
case "gpai":
|
||||
gpaiCount++
|
||||
default:
|
||||
t.Errorf("Unexpected axis '%s' for question %s", q.Axis, q.ID)
|
||||
}
|
||||
}
|
||||
if hrCount != 7 {
|
||||
t.Errorf("Expected 7 high_risk questions, got %d", hrCount)
|
||||
}
|
||||
if gpaiCount != 5 {
|
||||
t.Errorf("Expected 5 gpai questions, got %d", gpaiCount)
|
||||
}
|
||||
|
||||
// Check all questions have required fields
|
||||
for _, q := range tree.Questions {
|
||||
if q.ID == "" {
|
||||
t.Error("Question has empty ID")
|
||||
}
|
||||
if q.Question == "" {
|
||||
t.Errorf("Question %s has empty question text", q.ID)
|
||||
}
|
||||
if q.Description == "" {
|
||||
t.Errorf("Question %s has empty description", q.ID)
|
||||
}
|
||||
if q.ArticleRef == "" {
|
||||
t.Errorf("Question %s has empty article_ref", q.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateDecisionTree_NotApplicable(t *testing.T) {
|
||||
// Q1=No → AI Act not applicable
|
||||
req := &DecisionTreeEvalRequest{
|
||||
SystemName: "Test System",
|
||||
Answers: map[string]DecisionTreeAnswer{
|
||||
Q1: {QuestionID: Q1, Value: false},
|
||||
},
|
||||
}
|
||||
|
||||
result := EvaluateDecisionTree(req)
|
||||
|
||||
if result.HighRiskResult != AIActNotApplicable {
|
||||
t.Errorf("Expected not_applicable, got %s", result.HighRiskResult)
|
||||
}
|
||||
if result.GPAIResult.IsGPAI {
|
||||
t.Error("Expected GPAI to be false when Q8 is not answered")
|
||||
}
|
||||
if result.SystemName != "Test System" {
|
||||
t.Errorf("Expected system name 'Test System', got '%s'", result.SystemName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateDecisionTree_MinimalRisk(t *testing.T) {
|
||||
// Q1=Yes, Q2-Q7=No → minimal risk
|
||||
req := &DecisionTreeEvalRequest{
|
||||
SystemName: "Simple Tool",
|
||||
Answers: map[string]DecisionTreeAnswer{
|
||||
Q1: {QuestionID: Q1, Value: true},
|
||||
Q2: {QuestionID: Q2, Value: false},
|
||||
Q3: {QuestionID: Q3, Value: false},
|
||||
Q4: {QuestionID: Q4, Value: false},
|
||||
Q5: {QuestionID: Q5, Value: false},
|
||||
Q6: {QuestionID: Q6, Value: false},
|
||||
Q7: {QuestionID: Q7, Value: false},
|
||||
Q8: {QuestionID: Q8, Value: false},
|
||||
},
|
||||
}
|
||||
|
||||
result := EvaluateDecisionTree(req)
|
||||
|
||||
if result.HighRiskResult != AIActMinimalRisk {
|
||||
t.Errorf("Expected minimal_risk, got %s", result.HighRiskResult)
|
||||
}
|
||||
if result.GPAIResult.IsGPAI {
|
||||
t.Error("Expected GPAI to be false")
|
||||
}
|
||||
if result.GPAIResult.Category != GPAICategoryNone {
|
||||
t.Errorf("Expected GPAI category 'none', got '%s'", result.GPAIResult.Category)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateDecisionTree_HighRisk_Biometric(t *testing.T) {
|
||||
// Q1=Yes, Q2=Yes → high risk (biometric)
|
||||
req := &DecisionTreeEvalRequest{
|
||||
SystemName: "Face Recognition",
|
||||
Answers: map[string]DecisionTreeAnswer{
|
||||
Q1: {QuestionID: Q1, Value: true},
|
||||
Q2: {QuestionID: Q2, Value: true},
|
||||
Q3: {QuestionID: Q3, Value: false},
|
||||
Q4: {QuestionID: Q4, Value: false},
|
||||
Q5: {QuestionID: Q5, Value: false},
|
||||
Q6: {QuestionID: Q6, Value: false},
|
||||
Q7: {QuestionID: Q7, Value: false},
|
||||
},
|
||||
}
|
||||
|
||||
result := EvaluateDecisionTree(req)
|
||||
|
||||
if result.HighRiskResult != AIActHighRisk {
|
||||
t.Errorf("Expected high_risk, got %s", result.HighRiskResult)
|
||||
}
|
||||
|
||||
// Should have high-risk obligations
|
||||
if len(result.CombinedObligations) == 0 {
|
||||
t.Error("Expected non-empty obligations for high-risk system")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateDecisionTree_HighRisk_CriticalInfrastructure(t *testing.T) {
|
||||
// Q1=Yes, Q3=Yes → high risk (critical infrastructure)
|
||||
req := &DecisionTreeEvalRequest{
|
||||
SystemName: "Energy Grid AI",
|
||||
Answers: map[string]DecisionTreeAnswer{
|
||||
Q1: {QuestionID: Q1, Value: true},
|
||||
Q2: {QuestionID: Q2, Value: false},
|
||||
Q3: {QuestionID: Q3, Value: true},
|
||||
Q4: {QuestionID: Q4, Value: false},
|
||||
Q5: {QuestionID: Q5, Value: false},
|
||||
Q6: {QuestionID: Q6, Value: false},
|
||||
Q7: {QuestionID: Q7, Value: false},
|
||||
},
|
||||
}
|
||||
|
||||
result := EvaluateDecisionTree(req)
|
||||
|
||||
if result.HighRiskResult != AIActHighRisk {
|
||||
t.Errorf("Expected high_risk, got %s", result.HighRiskResult)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateDecisionTree_HighRisk_Education(t *testing.T) {
|
||||
// Q1=Yes, Q4=Yes → high risk (education/employment)
|
||||
req := &DecisionTreeEvalRequest{
|
||||
SystemName: "Exam Grading AI",
|
||||
Answers: map[string]DecisionTreeAnswer{
|
||||
Q1: {QuestionID: Q1, Value: true},
|
||||
Q2: {QuestionID: Q2, Value: false},
|
||||
Q3: {QuestionID: Q3, Value: false},
|
||||
Q4: {QuestionID: Q4, Value: true},
|
||||
},
|
||||
}
|
||||
|
||||
result := EvaluateDecisionTree(req)
|
||||
|
||||
if result.HighRiskResult != AIActHighRisk {
|
||||
t.Errorf("Expected high_risk, got %s", result.HighRiskResult)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateDecisionTree_HighRisk_AutonomousDecisions(t *testing.T) {
|
||||
// Q1=Yes, Q7=Yes → high risk (autonomous decisions)
|
||||
req := &DecisionTreeEvalRequest{
|
||||
SystemName: "Credit Scoring AI",
|
||||
Answers: map[string]DecisionTreeAnswer{
|
||||
Q1: {QuestionID: Q1, Value: true},
|
||||
Q2: {QuestionID: Q2, Value: false},
|
||||
Q3: {QuestionID: Q3, Value: false},
|
||||
Q4: {QuestionID: Q4, Value: false},
|
||||
Q5: {QuestionID: Q5, Value: false},
|
||||
Q6: {QuestionID: Q6, Value: false},
|
||||
Q7: {QuestionID: Q7, Value: true},
|
||||
},
|
||||
}
|
||||
|
||||
result := EvaluateDecisionTree(req)
|
||||
|
||||
if result.HighRiskResult != AIActHighRisk {
|
||||
t.Errorf("Expected high_risk, got %s", result.HighRiskResult)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateDecisionTree_GPAI_Standard(t *testing.T) {
|
||||
// Q8=Yes, Q10=No → GPAI standard
|
||||
req := &DecisionTreeEvalRequest{
|
||||
SystemName: "Custom LLM",
|
||||
Answers: map[string]DecisionTreeAnswer{
|
||||
Q1: {QuestionID: Q1, Value: true},
|
||||
Q8: {QuestionID: Q8, Value: true},
|
||||
Q9: {QuestionID: Q9, Value: true},
|
||||
Q10: {QuestionID: Q10, Value: false},
|
||||
Q11: {QuestionID: Q11, Value: false},
|
||||
Q12: {QuestionID: Q12, Value: false},
|
||||
},
|
||||
}
|
||||
|
||||
result := EvaluateDecisionTree(req)
|
||||
|
||||
if !result.GPAIResult.IsGPAI {
|
||||
t.Error("Expected IsGPAI to be true")
|
||||
}
|
||||
if result.GPAIResult.Category != GPAICategoryStandard {
|
||||
t.Errorf("Expected category 'standard', got '%s'", result.GPAIResult.Category)
|
||||
}
|
||||
if result.GPAIResult.IsSystemicRisk {
|
||||
t.Error("Expected IsSystemicRisk to be false")
|
||||
}
|
||||
|
||||
// Should have Art. 51, 53, 50 (generative)
|
||||
hasArt51 := false
|
||||
hasArt53 := false
|
||||
hasArt50 := false
|
||||
for _, a := range result.GPAIResult.ApplicableArticles {
|
||||
if a == "Art. 51" {
|
||||
hasArt51 = true
|
||||
}
|
||||
if a == "Art. 53" {
|
||||
hasArt53 = true
|
||||
}
|
||||
if a == "Art. 50" {
|
||||
hasArt50 = true
|
||||
}
|
||||
}
|
||||
if !hasArt51 {
|
||||
t.Error("Expected Art. 51 in applicable articles")
|
||||
}
|
||||
if !hasArt53 {
|
||||
t.Error("Expected Art. 53 in applicable articles")
|
||||
}
|
||||
if !hasArt50 {
|
||||
t.Error("Expected Art. 50 in applicable articles (generative AI)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateDecisionTree_GPAI_SystemicRisk(t *testing.T) {
|
||||
// Q8=Yes, Q10=Yes → GPAI systemic risk
|
||||
req := &DecisionTreeEvalRequest{
|
||||
SystemName: "GPT-5",
|
||||
Answers: map[string]DecisionTreeAnswer{
|
||||
Q1: {QuestionID: Q1, Value: true},
|
||||
Q8: {QuestionID: Q8, Value: true},
|
||||
Q9: {QuestionID: Q9, Value: true},
|
||||
Q10: {QuestionID: Q10, Value: true},
|
||||
Q11: {QuestionID: Q11, Value: true},
|
||||
Q12: {QuestionID: Q12, Value: true},
|
||||
},
|
||||
}
|
||||
|
||||
result := EvaluateDecisionTree(req)
|
||||
|
||||
if !result.GPAIResult.IsGPAI {
|
||||
t.Error("Expected IsGPAI to be true")
|
||||
}
|
||||
if result.GPAIResult.Category != GPAICategorySystemic {
|
||||
t.Errorf("Expected category 'systemic', got '%s'", result.GPAIResult.Category)
|
||||
}
|
||||
if !result.GPAIResult.IsSystemicRisk {
|
||||
t.Error("Expected IsSystemicRisk to be true")
|
||||
}
|
||||
|
||||
// Should have Art. 55
|
||||
hasArt55 := false
|
||||
for _, a := range result.GPAIResult.ApplicableArticles {
|
||||
if a == "Art. 55" {
|
||||
hasArt55 = true
|
||||
}
|
||||
}
|
||||
if !hasArt55 {
|
||||
t.Error("Expected Art. 55 in applicable articles (systemic risk)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateDecisionTree_Combined_HighRiskAndGPAI(t *testing.T) {
|
||||
// Q1=Yes, Q4=Yes (high risk) + Q8=Yes, Q9=Yes (GPAI standard)
|
||||
req := &DecisionTreeEvalRequest{
|
||||
SystemName: "HR Screening with LLM",
|
||||
SystemDescription: "LLM-based applicant screening system",
|
||||
Answers: map[string]DecisionTreeAnswer{
|
||||
Q1: {QuestionID: Q1, Value: true},
|
||||
Q2: {QuestionID: Q2, Value: false},
|
||||
Q3: {QuestionID: Q3, Value: false},
|
||||
Q4: {QuestionID: Q4, Value: true},
|
||||
Q5: {QuestionID: Q5, Value: false},
|
||||
Q6: {QuestionID: Q6, Value: false},
|
||||
Q7: {QuestionID: Q7, Value: true},
|
||||
Q8: {QuestionID: Q8, Value: true},
|
||||
Q9: {QuestionID: Q9, Value: true},
|
||||
Q10: {QuestionID: Q10, Value: false},
|
||||
Q11: {QuestionID: Q11, Value: false},
|
||||
Q12: {QuestionID: Q12, Value: false},
|
||||
},
|
||||
}
|
||||
|
||||
result := EvaluateDecisionTree(req)
|
||||
|
||||
// Both axes should be triggered
|
||||
if result.HighRiskResult != AIActHighRisk {
|
||||
t.Errorf("Expected high_risk, got %s", result.HighRiskResult)
|
||||
}
|
||||
if !result.GPAIResult.IsGPAI {
|
||||
t.Error("Expected GPAI to be true")
|
||||
}
|
||||
if result.GPAIResult.Category != GPAICategoryStandard {
|
||||
t.Errorf("Expected GPAI category 'standard', got '%s'", result.GPAIResult.Category)
|
||||
}
|
||||
|
||||
// Combined obligations should include both axes
|
||||
if len(result.CombinedObligations) < 5 {
|
||||
t.Errorf("Expected at least 5 combined obligations, got %d", len(result.CombinedObligations))
|
||||
}
|
||||
|
||||
// Should have articles from both axes
|
||||
if len(result.ApplicableArticles) < 3 {
|
||||
t.Errorf("Expected at least 3 applicable articles, got %d", len(result.ApplicableArticles))
|
||||
}
|
||||
|
||||
// Check system name preserved
|
||||
if result.SystemName != "HR Screening with LLM" {
|
||||
t.Errorf("Expected system name preserved, got '%s'", result.SystemName)
|
||||
}
|
||||
if result.SystemDescription != "LLM-based applicant screening system" {
|
||||
t.Errorf("Expected description preserved, got '%s'", result.SystemDescription)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateDecisionTree_GPAI_MarketPenetration(t *testing.T) {
|
||||
// Q8=Yes, Q10=No, Q12=Yes → GPAI standard with market penetration warning
|
||||
req := &DecisionTreeEvalRequest{
|
||||
SystemName: "Popular Chatbot",
|
||||
Answers: map[string]DecisionTreeAnswer{
|
||||
Q1: {QuestionID: Q1, Value: true},
|
||||
Q8: {QuestionID: Q8, Value: true},
|
||||
Q9: {QuestionID: Q9, Value: true},
|
||||
Q10: {QuestionID: Q10, Value: false},
|
||||
Q11: {QuestionID: Q11, Value: true},
|
||||
Q12: {QuestionID: Q12, Value: true},
|
||||
},
|
||||
}
|
||||
|
||||
result := EvaluateDecisionTree(req)
|
||||
|
||||
if result.GPAIResult.Category != GPAICategoryStandard {
|
||||
t.Errorf("Expected category 'standard' (not systemic because Q10=No), got '%s'", result.GPAIResult.Category)
|
||||
}
|
||||
|
||||
// Should have Art. 51 Abs. 3 warning
|
||||
hasArt51_3 := false
|
||||
for _, a := range result.GPAIResult.ApplicableArticles {
|
||||
if a == "Art. 51 Abs. 3" {
|
||||
hasArt51_3 = true
|
||||
}
|
||||
}
|
||||
if !hasArt51_3 {
|
||||
t.Error("Expected Art. 51 Abs. 3 in applicable articles for high market penetration")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateDecisionTree_NoGPAI(t *testing.T) {
|
||||
// Q8=No → No GPAI classification
|
||||
req := &DecisionTreeEvalRequest{
|
||||
SystemName: "Traditional ML",
|
||||
Answers: map[string]DecisionTreeAnswer{
|
||||
Q1: {QuestionID: Q1, Value: true},
|
||||
Q8: {QuestionID: Q8, Value: false},
|
||||
},
|
||||
}
|
||||
|
||||
result := EvaluateDecisionTree(req)
|
||||
|
||||
if result.GPAIResult.IsGPAI {
|
||||
t.Error("Expected IsGPAI to be false")
|
||||
}
|
||||
if result.GPAIResult.Category != GPAICategoryNone {
|
||||
t.Errorf("Expected category 'none', got '%s'", result.GPAIResult.Category)
|
||||
}
|
||||
if len(result.GPAIResult.Obligations) != 0 {
|
||||
t.Errorf("Expected 0 GPAI obligations, got %d", len(result.GPAIResult.Obligations))
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnswerIsYes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
answers map[string]DecisionTreeAnswer
|
||||
qID string
|
||||
expected bool
|
||||
}{
|
||||
{"yes answer", map[string]DecisionTreeAnswer{"Q1": {Value: true}}, "Q1", true},
|
||||
{"no answer", map[string]DecisionTreeAnswer{"Q1": {Value: false}}, "Q1", false},
|
||||
{"missing answer", map[string]DecisionTreeAnswer{}, "Q1", false},
|
||||
{"different question", map[string]DecisionTreeAnswer{"Q2": {Value: true}}, "Q1", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := answerIsYes(tt.answers, tt.qID)
|
||||
if result != tt.expected {
|
||||
t.Errorf("Expected %v, got %v", tt.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -525,3 +525,73 @@ const (
|
||||
ExportFormatJSON ExportFormat = "json"
|
||||
ExportFormatMarkdown ExportFormat = "md"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// AI Act Decision Tree Types
|
||||
// ============================================================================
|
||||
|
||||
// GPAICategory represents the GPAI classification result
|
||||
type GPAICategory string
|
||||
|
||||
const (
|
||||
GPAICategoryNone GPAICategory = "none"
|
||||
GPAICategoryStandard GPAICategory = "standard"
|
||||
GPAICategorySystemic GPAICategory = "systemic"
|
||||
)
|
||||
|
||||
// GPAIClassification represents the result of the GPAI axis evaluation
|
||||
type GPAIClassification struct {
|
||||
IsGPAI bool `json:"is_gpai"`
|
||||
IsSystemicRisk bool `json:"is_systemic_risk"`
|
||||
Category GPAICategory `json:"gpai_category"`
|
||||
ApplicableArticles []string `json:"applicable_articles"`
|
||||
Obligations []string `json:"obligations"`
|
||||
}
|
||||
|
||||
// DecisionTreeAnswer represents a user's answer to a decision tree question
|
||||
type DecisionTreeAnswer struct {
|
||||
QuestionID string `json:"question_id"`
|
||||
Value bool `json:"value"`
|
||||
Note string `json:"note,omitempty"`
|
||||
}
|
||||
|
||||
// DecisionTreeQuestion represents a single question in the decision tree
|
||||
type DecisionTreeQuestion struct {
|
||||
ID string `json:"id"`
|
||||
Axis string `json:"axis"` // "high_risk" or "gpai"
|
||||
Question string `json:"question"`
|
||||
Description string `json:"description"` // Additional context
|
||||
ArticleRef string `json:"article_ref"` // e.g., "Art. 5", "Anhang III"
|
||||
SkipIf string `json:"skip_if,omitempty"` // Question ID — skip if that was answered "no"
|
||||
}
|
||||
|
||||
// DecisionTreeDefinition represents the full decision tree structure for the frontend
|
||||
type DecisionTreeDefinition struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Questions []DecisionTreeQuestion `json:"questions"`
|
||||
}
|
||||
|
||||
// DecisionTreeEvalRequest is the API request for evaluating the decision tree
|
||||
type DecisionTreeEvalRequest struct {
|
||||
SystemName string `json:"system_name"`
|
||||
SystemDescription string `json:"system_description,omitempty"`
|
||||
Answers map[string]DecisionTreeAnswer `json:"answers"`
|
||||
}
|
||||
|
||||
// DecisionTreeResult represents the combined evaluation result
|
||||
type DecisionTreeResult struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
TenantID uuid.UUID `json:"tenant_id"`
|
||||
ProjectID *uuid.UUID `json:"project_id,omitempty"`
|
||||
SystemName string `json:"system_name"`
|
||||
SystemDescription string `json:"system_description,omitempty"`
|
||||
Answers map[string]DecisionTreeAnswer `json:"answers"`
|
||||
HighRiskResult AIActRiskLevel `json:"high_risk_result"`
|
||||
GPAIResult GPAIClassification `json:"gpai_result"`
|
||||
CombinedObligations []string `json:"combined_obligations"`
|
||||
ApplicableArticles []string `json:"applicable_articles"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
@@ -358,6 +358,128 @@ type AssessmentFilters struct {
|
||||
Offset int // OFFSET for pagination
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Decision Tree Result CRUD
|
||||
// ============================================================================
|
||||
|
||||
// CreateDecisionTreeResult stores a new decision tree result
|
||||
func (s *Store) CreateDecisionTreeResult(ctx context.Context, r *DecisionTreeResult) error {
|
||||
r.ID = uuid.New()
|
||||
r.CreatedAt = time.Now().UTC()
|
||||
r.UpdatedAt = r.CreatedAt
|
||||
|
||||
answers, _ := json.Marshal(r.Answers)
|
||||
gpaiResult, _ := json.Marshal(r.GPAIResult)
|
||||
obligations, _ := json.Marshal(r.CombinedObligations)
|
||||
articles, _ := json.Marshal(r.ApplicableArticles)
|
||||
|
||||
_, err := s.pool.Exec(ctx, `
|
||||
INSERT INTO ai_act_decision_tree_results (
|
||||
id, tenant_id, project_id, system_name, system_description,
|
||||
answers, high_risk_level, gpai_result,
|
||||
combined_obligations, applicable_articles,
|
||||
created_at, updated_at
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5,
|
||||
$6, $7, $8,
|
||||
$9, $10,
|
||||
$11, $12
|
||||
)
|
||||
`,
|
||||
r.ID, r.TenantID, r.ProjectID, r.SystemName, r.SystemDescription,
|
||||
answers, string(r.HighRiskResult), gpaiResult,
|
||||
obligations, articles,
|
||||
r.CreatedAt, r.UpdatedAt,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetDecisionTreeResult retrieves a decision tree result by ID
|
||||
func (s *Store) GetDecisionTreeResult(ctx context.Context, id uuid.UUID) (*DecisionTreeResult, error) {
|
||||
var r DecisionTreeResult
|
||||
var answersBytes, gpaiBytes, oblBytes, artBytes []byte
|
||||
var highRiskLevel string
|
||||
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
SELECT id, tenant_id, project_id, system_name, system_description,
|
||||
answers, high_risk_level, gpai_result,
|
||||
combined_obligations, applicable_articles,
|
||||
created_at, updated_at
|
||||
FROM ai_act_decision_tree_results WHERE id = $1
|
||||
`, id).Scan(
|
||||
&r.ID, &r.TenantID, &r.ProjectID, &r.SystemName, &r.SystemDescription,
|
||||
&answersBytes, &highRiskLevel, &gpaiBytes,
|
||||
&oblBytes, &artBytes,
|
||||
&r.CreatedAt, &r.UpdatedAt,
|
||||
)
|
||||
|
||||
if err == pgx.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
json.Unmarshal(answersBytes, &r.Answers)
|
||||
json.Unmarshal(gpaiBytes, &r.GPAIResult)
|
||||
json.Unmarshal(oblBytes, &r.CombinedObligations)
|
||||
json.Unmarshal(artBytes, &r.ApplicableArticles)
|
||||
r.HighRiskResult = AIActRiskLevel(highRiskLevel)
|
||||
|
||||
return &r, nil
|
||||
}
|
||||
|
||||
// ListDecisionTreeResults lists all decision tree results for a tenant
|
||||
func (s *Store) ListDecisionTreeResults(ctx context.Context, tenantID uuid.UUID) ([]DecisionTreeResult, error) {
|
||||
rows, err := s.pool.Query(ctx, `
|
||||
SELECT id, tenant_id, project_id, system_name, system_description,
|
||||
answers, high_risk_level, gpai_result,
|
||||
combined_obligations, applicable_articles,
|
||||
created_at, updated_at
|
||||
FROM ai_act_decision_tree_results
|
||||
WHERE tenant_id = $1
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 100
|
||||
`, tenantID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var results []DecisionTreeResult
|
||||
for rows.Next() {
|
||||
var r DecisionTreeResult
|
||||
var answersBytes, gpaiBytes, oblBytes, artBytes []byte
|
||||
var highRiskLevel string
|
||||
|
||||
err := rows.Scan(
|
||||
&r.ID, &r.TenantID, &r.ProjectID, &r.SystemName, &r.SystemDescription,
|
||||
&answersBytes, &highRiskLevel, &gpaiBytes,
|
||||
&oblBytes, &artBytes,
|
||||
&r.CreatedAt, &r.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
json.Unmarshal(answersBytes, &r.Answers)
|
||||
json.Unmarshal(gpaiBytes, &r.GPAIResult)
|
||||
json.Unmarshal(oblBytes, &r.CombinedObligations)
|
||||
json.Unmarshal(artBytes, &r.ApplicableArticles)
|
||||
r.HighRiskResult = AIActRiskLevel(highRiskLevel)
|
||||
|
||||
results = append(results, r)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
|
||||
// DeleteDecisionTreeResult deletes a decision tree result by ID
|
||||
func (s *Store) DeleteDecisionTreeResult(ctx context.Context, id uuid.UUID) error {
|
||||
_, err := s.pool.Exec(ctx, "DELETE FROM ai_act_decision_tree_results WHERE id = $1", id)
|
||||
return err
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Helpers
|
||||
// ============================================================================
|
||||
|
||||
Reference in New Issue
Block a user