This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/ai-compliance-sdk/internal/api/handlers/drafting_handlers.go
BreakPilot Dev 206183670d feat(sdk): Add Drafting Engine with 4-mode agent system (Explain/Ask/Draft/Validate)
Extends the Compliance Advisor from a Q&A chatbot into a full drafting engine
that can generate, validate, and refine compliance documents within Scope Engine
constraints. Includes intent classifier, state projector, constraint enforcer,
SOUL templates, Go backend endpoints, and React UI components.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 12:37:18 +01:00

336 lines
10 KiB
Go

package handlers
import (
"fmt"
"net/http"
"time"
"github.com/breakpilot/ai-compliance-sdk/internal/audit"
"github.com/breakpilot/ai-compliance-sdk/internal/llm"
"github.com/breakpilot/ai-compliance-sdk/internal/rbac"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
// DraftingHandlers handles Drafting Engine API endpoints
type DraftingHandlers struct {
accessGate *llm.AccessGate
registry *llm.ProviderRegistry
piiDetector *llm.PIIDetector
auditStore *audit.Store
trailBuilder *audit.TrailBuilder
}
// NewDraftingHandlers creates new Drafting Engine handlers
func NewDraftingHandlers(
accessGate *llm.AccessGate,
registry *llm.ProviderRegistry,
piiDetector *llm.PIIDetector,
auditStore *audit.Store,
trailBuilder *audit.TrailBuilder,
) *DraftingHandlers {
return &DraftingHandlers{
accessGate: accessGate,
registry: registry,
piiDetector: piiDetector,
auditStore: auditStore,
trailBuilder: trailBuilder,
}
}
// ---------------------------------------------------------------------------
// Request/Response Types
// ---------------------------------------------------------------------------
// DraftDocumentRequest represents a request to generate a compliance document draft
type DraftDocumentRequest struct {
DocumentType string `json:"document_type" binding:"required"`
ScopeLevel string `json:"scope_level" binding:"required"`
Context map[string]interface{} `json:"context"`
Instructions string `json:"instructions"`
Model string `json:"model"`
}
// ValidateDocumentRequest represents a request to validate document consistency
type ValidateDocumentRequest struct {
DocumentType string `json:"document_type" binding:"required"`
DraftContent string `json:"draft_content"`
ValidationContext map[string]interface{} `json:"validation_context"`
Model string `json:"model"`
}
// DraftHistoryEntry represents a single audit trail entry for drafts
type DraftHistoryEntry struct {
ID string `json:"id"`
UserID string `json:"user_id"`
TenantID string `json:"tenant_id"`
DocumentType string `json:"document_type"`
ScopeLevel string `json:"scope_level"`
Operation string `json:"operation"`
ConstraintsRespected bool `json:"constraints_respected"`
TokensUsed int `json:"tokens_used"`
CreatedAt time.Time `json:"created_at"`
}
// ---------------------------------------------------------------------------
// Handlers
// ---------------------------------------------------------------------------
// DraftDocument handles document draft generation via LLM with constraint validation
func (h *DraftingHandlers) DraftDocument(c *gin.Context) {
var req DraftDocumentRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
userID := rbac.GetUserID(c)
tenantID := rbac.GetTenantID(c)
namespaceID := rbac.GetNamespaceID(c)
if userID == uuid.Nil || tenantID == uuid.Nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "authentication required"})
return
}
// Validate scope level
validLevels := map[string]bool{"L1": true, "L2": true, "L3": true, "L4": true}
if !validLevels[req.ScopeLevel] {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid scope_level, must be L1-L4"})
return
}
// Validate document type
validTypes := map[string]bool{
"vvt": true, "tom": true, "dsfa": true, "dsi": true, "lf": true,
"av_vertrag": true, "betroffenenrechte": true, "einwilligung": true,
"daten_transfer": true, "datenpannen": true, "vertragsmanagement": true,
"schulung": true, "audit_log": true, "risikoanalyse": true,
"notfallplan": true, "zertifizierung": true, "datenschutzmanagement": true,
}
if !validTypes[req.DocumentType] {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid document_type"})
return
}
// Build system prompt for drafting
systemPrompt := fmt.Sprintf(
`Du bist ein DSGVO-Compliance-Experte. Erstelle einen strukturierten Entwurf fuer Dokument "%s" auf Level %s.
Antworte NUR im JSON-Format mit einem "sections" Array.
Jede Section hat: id, title, content, schemaField.
Halte die Tiefe strikt am vorgegebenen Level.
Markiere fehlende Informationen mit [PLATZHALTER: Beschreibung].
Sprache: Deutsch.`,
req.DocumentType, req.ScopeLevel,
)
userPrompt := "Erstelle den Dokumententwurf."
if req.Instructions != "" {
userPrompt = req.Instructions
}
// Detect PII in context
contextStr := fmt.Sprintf("%v", req.Context)
dataCategories := h.piiDetector.DetectDataCategories(contextStr)
// Process through access gate
chatReq := &llm.ChatRequest{
Model: req.Model,
Messages: []llm.Message{
{Role: "system", Content: systemPrompt},
{Role: "user", Content: userPrompt},
},
MaxTokens: 16384,
Temperature: 0.15,
}
gatedReq, err := h.accessGate.ProcessChatRequest(
c.Request.Context(),
userID, tenantID, namespaceID,
chatReq, dataCategories,
)
if err != nil {
h.logDraftAudit(c, userID, tenantID, req.DocumentType, req.ScopeLevel, "draft", false, 0, err.Error())
c.JSON(http.StatusForbidden, gin.H{
"error": "access_denied",
"message": err.Error(),
})
return
}
// Execute the request
resp, err := h.accessGate.ExecuteChat(c.Request.Context(), gatedReq)
if err != nil {
h.logDraftAudit(c, userID, tenantID, req.DocumentType, req.ScopeLevel, "draft", false, 0, err.Error())
c.JSON(http.StatusInternalServerError, gin.H{
"error": "llm_error",
"message": err.Error(),
})
return
}
tokensUsed := 0
if resp.Usage.TotalTokens > 0 {
tokensUsed = resp.Usage.TotalTokens
}
// Log successful draft
h.logDraftAudit(c, userID, tenantID, req.DocumentType, req.ScopeLevel, "draft", true, tokensUsed, "")
c.JSON(http.StatusOK, gin.H{
"document_type": req.DocumentType,
"scope_level": req.ScopeLevel,
"content": resp.Message.Content,
"model": resp.Model,
"provider": resp.Provider,
"tokens_used": tokensUsed,
"pii_detected": gatedReq.PIIDetected,
})
}
// ValidateDocument handles document cross-consistency validation
func (h *DraftingHandlers) ValidateDocument(c *gin.Context) {
var req ValidateDocumentRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
userID := rbac.GetUserID(c)
tenantID := rbac.GetTenantID(c)
namespaceID := rbac.GetNamespaceID(c)
if userID == uuid.Nil || tenantID == uuid.Nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "authentication required"})
return
}
// Build validation prompt
systemPrompt := `Du bist ein DSGVO-Compliance-Validator. Pruefe die Konsistenz und Vollstaendigkeit.
Antworte NUR im JSON-Format:
{
"passed": boolean,
"errors": [{"id": string, "severity": "error", "title": string, "description": string, "documentType": string, "legalReference": string}],
"warnings": [{"id": string, "severity": "warning", "title": string, "description": string}],
"suggestions": [{"id": string, "severity": "suggestion", "title": string, "description": string, "suggestion": string}]
}`
validationPrompt := fmt.Sprintf("Validiere Dokument '%s'.\nInhalt:\n%s\nKontext:\n%v",
req.DocumentType, req.DraftContent, req.ValidationContext)
// Detect PII
dataCategories := h.piiDetector.DetectDataCategories(req.DraftContent)
chatReq := &llm.ChatRequest{
Model: req.Model,
Messages: []llm.Message{
{Role: "system", Content: systemPrompt},
{Role: "user", Content: validationPrompt},
},
MaxTokens: 8192,
Temperature: 0.1,
}
gatedReq, err := h.accessGate.ProcessChatRequest(
c.Request.Context(),
userID, tenantID, namespaceID,
chatReq, dataCategories,
)
if err != nil {
h.logDraftAudit(c, userID, tenantID, req.DocumentType, "", "validate", false, 0, err.Error())
c.JSON(http.StatusForbidden, gin.H{
"error": "access_denied",
"message": err.Error(),
})
return
}
resp, err := h.accessGate.ExecuteChat(c.Request.Context(), gatedReq)
if err != nil {
h.logDraftAudit(c, userID, tenantID, req.DocumentType, "", "validate", false, 0, err.Error())
c.JSON(http.StatusInternalServerError, gin.H{
"error": "llm_error",
"message": err.Error(),
})
return
}
tokensUsed := 0
if resp.Usage.TotalTokens > 0 {
tokensUsed = resp.Usage.TotalTokens
}
h.logDraftAudit(c, userID, tenantID, req.DocumentType, "", "validate", true, tokensUsed, "")
c.JSON(http.StatusOK, gin.H{
"document_type": req.DocumentType,
"validation": resp.Message.Content,
"model": resp.Model,
"provider": resp.Provider,
"tokens_used": tokensUsed,
"pii_detected": gatedReq.PIIDetected,
})
}
// GetDraftHistory returns the audit trail of all drafting operations for a tenant
func (h *DraftingHandlers) GetDraftHistory(c *gin.Context) {
userID := rbac.GetUserID(c)
tenantID := rbac.GetTenantID(c)
if userID == uuid.Nil || tenantID == uuid.Nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "authentication required"})
return
}
// Query audit store for drafting operations
entries, _, err := h.auditStore.QueryGeneralAuditEntries(c.Request.Context(), &audit.GeneralAuditFilter{
TenantID: tenantID,
ResourceType: "compliance_document",
Limit: 50,
})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to query draft history"})
return
}
c.JSON(http.StatusOK, gin.H{
"history": entries,
"total": len(entries),
})
}
// ---------------------------------------------------------------------------
// Audit Logging
// ---------------------------------------------------------------------------
func (h *DraftingHandlers) logDraftAudit(
c *gin.Context,
userID, tenantID uuid.UUID,
documentType, scopeLevel, operation string,
constraintsRespected bool,
tokensUsed int,
errorMsg string,
) {
newValues := map[string]any{
"document_type": documentType,
"scope_level": scopeLevel,
"constraints_respected": constraintsRespected,
"tokens_used": tokensUsed,
}
if errorMsg != "" {
newValues["error"] = errorMsg
}
entry := h.trailBuilder.NewGeneralEntry().
WithTenant(tenantID).
WithUser(userID).
WithAction("drafting_engine." + operation).
WithResource("compliance_document", nil).
WithNewValues(newValues).
WithClient(c.ClientIP(), c.GetHeader("User-Agent"))
go func() {
entry.Save(c.Request.Context())
}()
}