diff --git a/admin-v2/app/api/sdk/drafting-engine/chat/route.ts b/admin-v2/app/api/sdk/drafting-engine/chat/route.ts new file mode 100644 index 0000000..e24b211 --- /dev/null +++ b/admin-v2/app/api/sdk/drafting-engine/chat/route.ts @@ -0,0 +1,184 @@ +/** + * Drafting Engine Chat API + * + * Verbindet das DraftingEngineWidget mit dem LLM Backend. + * Unterstuetzt alle 4 Modi: explain, ask, draft, validate. + * Nutzt State-Projection fuer token-effiziente Kontextgabe. + */ + +import { NextRequest, NextResponse } from 'next/server' + +const KLAUSUR_SERVICE_URL = process.env.KLAUSUR_SERVICE_URL || 'http://klausur-service:8086' +const OLLAMA_URL = process.env.OLLAMA_URL || 'http://host.docker.internal:11434' +const LLM_MODEL = process.env.COMPLIANCE_LLM_MODEL || 'qwen2.5vl:32b' + +// SOUL System Prompt (from agent-core/soul/drafting-agent.soul.md) +const DRAFTING_SYSTEM_PROMPT = `# Drafting Agent - Compliance-Dokumententwurf + +## Identitaet +Du bist der BreakPilot Drafting Agent. Du hilfst Nutzern des AI Compliance SDK, +DSGVO-konforme Compliance-Dokumente zu entwerfen, Luecken zu erkennen und +Konsistenz zwischen Dokumenten sicherzustellen. + +## Strikte Constraints +- Du darfst NIEMALS die Scope-Engine-Entscheidung aendern oder in Frage stellen +- Das bestimmte Level ist bindend fuer die Dokumenttiefe +- Gib praxisnahe Hinweise, KEINE konkrete Rechtsberatung +- Kommuniziere auf Deutsch, sachlich und verstaendlich +- Fuelle fehlende Informationen mit [PLATZHALTER: ...] Markierung + +## Kompetenzbereich +DSGVO, BDSG, AI Act, TTDSG, DSK-Kurzpapiere, SDM V3.0, BSI-Grundschutz, ISO 27001/27701, EDPB Guidelines, WP248` + +/** + * Query the RAG corpus for relevant documents + */ +async function queryRAG(query: string): Promise { + try { + const url = `${KLAUSUR_SERVICE_URL}/api/v1/dsfa-rag/search?query=${encodeURIComponent(query)}&top_k=3` + const res = await fetch(url, { + headers: { 'Content-Type': 'application/json' }, + signal: AbortSignal.timeout(10000), + }) + + if (!res.ok) return '' + + const data = await res.json() + if (data.results?.length > 0) { + return data.results + .map( + (r: { source_name?: string; source_code?: string; content?: string }, i: number) => + `[Quelle ${i + 1}: ${r.source_name || r.source_code || 'Unbekannt'}]\n${r.content || ''}` + ) + .join('\n\n---\n\n') + } + return '' + } catch { + return '' + } +} + +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const { + message, + history = [], + sdkStateProjection, + mode = 'explain', + documentType, + } = body + + if (!message || typeof message !== 'string') { + return NextResponse.json({ error: 'Message is required' }, { status: 400 }) + } + + // 1. Query RAG for legal context + const ragContext = await queryRAG(message) + + // 2. Build system prompt with mode-specific instructions + state projection + let systemContent = DRAFTING_SYSTEM_PROMPT + + // Mode-specific instructions + const modeInstructions: Record = { + explain: '\n\n## Aktueller Modus: EXPLAIN\nBeantworte Fragen verstaendlich mit Quellenangaben.', + ask: '\n\n## Aktueller Modus: ASK\nAnalysiere Luecken und stelle gezielte Fragen. Eine Frage pro Antwort.', + draft: `\n\n## Aktueller Modus: DRAFT\nEntwirf strukturierte Dokument-Sections. Dokumenttyp: ${documentType || 'nicht spezifiziert'}.\nAntworte mit JSON wenn ein Draft angefragt wird.`, + validate: '\n\n## Aktueller Modus: VALIDATE\nPruefe Cross-Dokument-Konsistenz. Gib Errors, Warnings und Suggestions zurueck.', + } + systemContent += modeInstructions[mode] || modeInstructions.explain + + // Add state projection context + if (sdkStateProjection) { + systemContent += `\n\n## SDK-State Projektion (${mode}-Kontext)\n${JSON.stringify(sdkStateProjection, null, 0).slice(0, 3000)}` + } + + // Add RAG context + if (ragContext) { + systemContent += `\n\n## Relevanter Rechtskontext\n${ragContext}` + } + + // 3. Build messages array + const messages = [ + { role: 'system', content: systemContent }, + ...history.slice(-10).map((h: { role: string; content: string }) => ({ + role: h.role === 'user' ? 'user' : 'assistant', + content: h.content, + })), + { role: 'user', content: message }, + ] + + // 4. Call LLM with streaming + const ollamaResponse = await fetch(`${OLLAMA_URL}/api/chat`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + model: LLM_MODEL, + messages, + stream: true, + options: { + temperature: mode === 'draft' ? 0.2 : 0.3, + num_predict: mode === 'draft' ? 16384 : 8192, + }, + }), + signal: AbortSignal.timeout(120000), + }) + + if (!ollamaResponse.ok) { + const errorText = await ollamaResponse.text() + console.error('LLM error:', ollamaResponse.status, errorText) + return NextResponse.json( + { error: `LLM nicht erreichbar (Status ${ollamaResponse.status})` }, + { status: 502 } + ) + } + + // 5. Stream response back + const encoder = new TextEncoder() + const stream = new ReadableStream({ + async start(controller) { + const reader = ollamaResponse.body!.getReader() + const decoder = new TextDecoder() + + try { + while (true) { + const { done, value } = await reader.read() + if (done) break + + const chunk = decoder.decode(value, { stream: true }) + const lines = chunk.split('\n').filter((l) => l.trim()) + + for (const line of lines) { + try { + const json = JSON.parse(line) + if (json.message?.content) { + controller.enqueue(encoder.encode(json.message.content)) + } + } catch { + // Partial JSON, skip + } + } + } + } catch (error) { + console.error('Stream error:', error) + } finally { + controller.close() + } + }, + }) + + return new NextResponse(stream, { + headers: { + 'Content-Type': 'text/plain; charset=utf-8', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + }, + }) + } catch (error) { + console.error('Drafting engine chat error:', error) + return NextResponse.json( + { error: 'Verbindung zum LLM fehlgeschlagen.' }, + { status: 503 } + ) + } +} diff --git a/admin-v2/app/api/sdk/drafting-engine/draft/route.ts b/admin-v2/app/api/sdk/drafting-engine/draft/route.ts new file mode 100644 index 0000000..c7a4a32 --- /dev/null +++ b/admin-v2/app/api/sdk/drafting-engine/draft/route.ts @@ -0,0 +1,168 @@ +/** + * Drafting Engine - Draft API + * + * Erstellt strukturierte Compliance-Dokument-Entwuerfe. + * Baut dokument-spezifische Prompts aus SOUL-Template + State-Projection. + * Gibt strukturiertes JSON zurueck. + */ + +import { NextRequest, NextResponse } from 'next/server' + +const OLLAMA_URL = process.env.OLLAMA_URL || 'http://host.docker.internal:11434' +const LLM_MODEL = process.env.COMPLIANCE_LLM_MODEL || 'qwen2.5vl:32b' + +// Import prompt builders +import { buildVVTDraftPrompt } from '@/lib/sdk/drafting-engine/prompts/draft-vvt' +import { buildTOMDraftPrompt } from '@/lib/sdk/drafting-engine/prompts/draft-tom' +import { buildDSFADraftPrompt } from '@/lib/sdk/drafting-engine/prompts/draft-dsfa' +import { buildPrivacyPolicyDraftPrompt } from '@/lib/sdk/drafting-engine/prompts/draft-privacy-policy' +import { buildLoeschfristenDraftPrompt } from '@/lib/sdk/drafting-engine/prompts/draft-loeschfristen' +import type { DraftContext, DraftResponse, DraftRevision, DraftSection } from '@/lib/sdk/drafting-engine/types' +import type { ScopeDocumentType } from '@/lib/sdk/compliance-scope-types' +import { ConstraintEnforcer } from '@/lib/sdk/drafting-engine/constraint-enforcer' + +const constraintEnforcer = new ConstraintEnforcer() + +const DRAFTING_SYSTEM_PROMPT = `Du bist ein DSGVO-Compliance-Experte und erstellst strukturierte Dokument-Entwuerfe. +Du MUSST immer im JSON-Format antworten 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.` + +function buildPromptForDocumentType( + documentType: ScopeDocumentType, + context: DraftContext, + instructions?: string +): string { + switch (documentType) { + case 'vvt': + return buildVVTDraftPrompt({ context, instructions }) + case 'tom': + return buildTOMDraftPrompt({ context, instructions }) + case 'dsfa': + return buildDSFADraftPrompt({ context, instructions }) + case 'dsi': + return buildPrivacyPolicyDraftPrompt({ context, instructions }) + case 'lf': + return buildLoeschfristenDraftPrompt({ context, instructions }) + default: + return `## Aufgabe: Entwurf fuer ${documentType} + +### Level: ${context.decisions.level} +### Tiefe: ${context.constraints.depthRequirements.depth} +### Erforderliche Inhalte: +${context.constraints.depthRequirements.detailItems.map((item, i) => `${i + 1}. ${item}`).join('\n')} + +${instructions ? `### Anweisungen: ${instructions}` : ''} + +Antworte als JSON mit "sections" Array.` + } +} + +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const { documentType, draftContext, instructions, existingDraft } = body + + if (!documentType || !draftContext) { + return NextResponse.json( + { error: 'documentType und draftContext sind erforderlich' }, + { status: 400 } + ) + } + + // 1. Constraint Check (Hard Gate) + const constraintCheck = constraintEnforcer.checkFromContext(documentType, draftContext) + + if (!constraintCheck.allowed) { + return NextResponse.json({ + draft: null, + constraintCheck, + tokensUsed: 0, + error: 'Constraint-Verletzung: ' + constraintCheck.violations.join('; '), + }, { status: 403 }) + } + + // 2. Build document-specific prompt + const draftPrompt = buildPromptForDocumentType(documentType, draftContext, instructions) + + // 3. Build messages + const messages = [ + { role: 'system', content: DRAFTING_SYSTEM_PROMPT }, + ...(existingDraft ? [{ + role: 'assistant', + content: `Bisheriger Entwurf:\n${JSON.stringify(existingDraft.sections, null, 2)}`, + }] : []), + { role: 'user', content: draftPrompt }, + ] + + // 4. Call LLM (non-streaming for structured output) + const ollamaResponse = await fetch(`${OLLAMA_URL}/api/chat`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + model: LLM_MODEL, + messages, + stream: false, + options: { + temperature: 0.15, + num_predict: 16384, + }, + format: 'json', + }), + signal: AbortSignal.timeout(180000), + }) + + if (!ollamaResponse.ok) { + return NextResponse.json( + { error: `LLM nicht erreichbar (Status ${ollamaResponse.status})` }, + { status: 502 } + ) + } + + const result = await ollamaResponse.json() + const content = result.message?.content || '' + + // 5. Parse JSON response + let sections: DraftSection[] = [] + try { + const parsed = JSON.parse(content) + sections = (parsed.sections || []).map((s: Record, i: number) => ({ + id: String(s.id || `section-${i}`), + title: String(s.title || ''), + content: String(s.content || ''), + schemaField: s.schemaField ? String(s.schemaField) : undefined, + })) + } catch { + // If not JSON, wrap raw content as single section + sections = [{ + id: 'raw', + title: 'Entwurf', + content: content, + }] + } + + const draft: DraftRevision = { + id: `draft-${Date.now()}`, + content: sections.map(s => `## ${s.title}\n\n${s.content}`).join('\n\n'), + sections, + createdAt: new Date().toISOString(), + instruction: instructions, + } + + const response: DraftResponse = { + draft, + constraintCheck, + tokensUsed: result.eval_count || 0, + } + + return NextResponse.json(response) + } catch (error) { + console.error('Draft generation error:', error) + return NextResponse.json( + { error: 'Draft-Generierung fehlgeschlagen.' }, + { status: 503 } + ) + } +} diff --git a/admin-v2/app/api/sdk/drafting-engine/validate/route.ts b/admin-v2/app/api/sdk/drafting-engine/validate/route.ts new file mode 100644 index 0000000..521f5fe --- /dev/null +++ b/admin-v2/app/api/sdk/drafting-engine/validate/route.ts @@ -0,0 +1,188 @@ +/** + * Drafting Engine - Validate API + * + * Stufe 1: Deterministische Pruefung gegen DOCUMENT_SCOPE_MATRIX + * Stufe 2: LLM Cross-Consistency Check + */ + +import { NextRequest, NextResponse } from 'next/server' +import { DOCUMENT_SCOPE_MATRIX, DOCUMENT_TYPE_LABELS, getDepthLevelNumeric } from '@/lib/sdk/compliance-scope-types' +import type { ScopeDocumentType, ComplianceDepthLevel } from '@/lib/sdk/compliance-scope-types' +import type { ValidationContext, ValidationResult, ValidationFinding } from '@/lib/sdk/drafting-engine/types' +import { buildCrossCheckPrompt } from '@/lib/sdk/drafting-engine/prompts/validate-cross-check' + +const OLLAMA_URL = process.env.OLLAMA_URL || 'http://host.docker.internal:11434' +const LLM_MODEL = process.env.COMPLIANCE_LLM_MODEL || 'qwen2.5vl:32b' + +/** + * Stufe 1: Deterministische Pruefung + */ +function deterministicCheck( + documentType: ScopeDocumentType, + validationContext: ValidationContext +): ValidationFinding[] { + const findings: ValidationFinding[] = [] + const level = validationContext.scopeLevel + const levelNumeric = getDepthLevelNumeric(level) + const req = DOCUMENT_SCOPE_MATRIX[documentType]?.[level] + + // Check 1: Ist das Dokument auf diesem Level erforderlich? + if (req && !req.required && levelNumeric < 3) { + findings.push({ + id: `DET-OPT-${documentType}`, + severity: 'suggestion', + category: 'scope_violation', + title: `${DOCUMENT_TYPE_LABELS[documentType] ?? documentType} ist optional`, + description: `Auf Level ${level} ist dieses Dokument nicht verpflichtend.`, + documentType, + }) + } + + // Check 2: VVT vorhanden wenn erforderlich? + const vvtReq = DOCUMENT_SCOPE_MATRIX.vvt[level] + if (vvtReq.required && validationContext.crossReferences.vvtCategories.length === 0) { + findings.push({ + id: 'DET-VVT-MISSING', + severity: 'error', + category: 'missing_content', + title: 'VVT fehlt', + description: `Auf Level ${level} ist ein VVT Pflicht, aber keine Eintraege vorhanden.`, + documentType: 'vvt', + legalReference: 'Art. 30 DSGVO', + }) + } + + // Check 3: TOM vorhanden wenn VVT existiert? + if (validationContext.crossReferences.vvtCategories.length > 0 + && validationContext.crossReferences.tomControls.length === 0) { + findings.push({ + id: 'DET-TOM-MISSING-FOR-VVT', + severity: 'warning', + category: 'cross_reference', + title: 'TOM fehlt bei vorhandenem VVT', + description: 'VVT-Eintraege existieren, aber keine TOM-Massnahmen sind definiert.', + documentType: 'tom', + crossReferenceType: 'vvt', + legalReference: 'Art. 32 DSGVO', + suggestion: 'TOM-Massnahmen erstellen, die die VVT-Taetigkeiten absichern.', + }) + } + + // Check 4: Loeschfristen fuer VVT-Kategorien + if (validationContext.crossReferences.vvtCategories.length > 0 + && validationContext.crossReferences.retentionCategories.length === 0) { + findings.push({ + id: 'DET-LF-MISSING-FOR-VVT', + severity: 'warning', + category: 'cross_reference', + title: 'Loeschfristen fehlen', + description: 'VVT-Eintraege existieren, aber keine Loeschfristen sind definiert.', + documentType: 'lf', + crossReferenceType: 'vvt', + legalReference: 'Art. 17 DSGVO', + suggestion: 'Loeschfristen fuer alle VVT-Datenkategorien definieren.', + }) + } + + return findings +} + +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const { documentType, draftContent, validationContext } = body + + if (!documentType || !validationContext) { + return NextResponse.json( + { error: 'documentType und validationContext sind erforderlich' }, + { status: 400 } + ) + } + + // --------------------------------------------------------------- + // Stufe 1: Deterministische Pruefung + // --------------------------------------------------------------- + const deterministicFindings = deterministicCheck(documentType, validationContext) + + // --------------------------------------------------------------- + // Stufe 2: LLM Cross-Consistency Check + // --------------------------------------------------------------- + let llmFindings: ValidationFinding[] = [] + + try { + const crossCheckPrompt = buildCrossCheckPrompt({ + context: validationContext, + }) + + const ollamaResponse = await fetch(`${OLLAMA_URL}/api/chat`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + model: LLM_MODEL, + messages: [ + { + role: 'system', + content: 'Du bist ein DSGVO-Compliance-Validator. Antworte NUR im JSON-Format.', + }, + { role: 'user', content: crossCheckPrompt }, + ], + stream: false, + options: { temperature: 0.1, num_predict: 8192 }, + format: 'json', + }), + signal: AbortSignal.timeout(120000), + }) + + if (ollamaResponse.ok) { + const result = await ollamaResponse.json() + try { + const parsed = JSON.parse(result.message?.content || '{}') + llmFindings = [ + ...(parsed.errors || []), + ...(parsed.warnings || []), + ...(parsed.suggestions || []), + ].map((f: Record, i: number) => ({ + id: String(f.id || `LLM-${i}`), + severity: (String(f.severity || 'suggestion')) as 'error' | 'warning' | 'suggestion', + category: (String(f.category || 'inconsistency')) as ValidationFinding['category'], + title: String(f.title || ''), + description: String(f.description || ''), + documentType: (String(f.documentType || documentType)) as ScopeDocumentType, + crossReferenceType: f.crossReferenceType ? String(f.crossReferenceType) as ScopeDocumentType : undefined, + legalReference: f.legalReference ? String(f.legalReference) : undefined, + suggestion: f.suggestion ? String(f.suggestion) : undefined, + })) + } catch { + // LLM response not parseable, skip + } + } + } catch { + // LLM unavailable, continue with deterministic results only + } + + // --------------------------------------------------------------- + // Combine results + // --------------------------------------------------------------- + const allFindings = [...deterministicFindings, ...llmFindings] + const errors = allFindings.filter(f => f.severity === 'error') + const warnings = allFindings.filter(f => f.severity === 'warning') + const suggestions = allFindings.filter(f => f.severity === 'suggestion') + + const result: ValidationResult = { + passed: errors.length === 0, + timestamp: new Date().toISOString(), + scopeLevel: validationContext.scopeLevel, + errors, + warnings, + suggestions, + } + + return NextResponse.json(result) + } catch (error) { + console.error('Validation error:', error) + return NextResponse.json( + { error: 'Validierung fehlgeschlagen.' }, + { status: 503 } + ) + } +} diff --git a/admin-v2/components/sdk/ComplianceAdvisorWidget.tsx b/admin-v2/components/sdk/ComplianceAdvisorWidget.tsx index 650fb63..65b1eab 100644 --- a/admin-v2/components/sdk/ComplianceAdvisorWidget.tsx +++ b/admin-v2/components/sdk/ComplianceAdvisorWidget.tsx @@ -15,6 +15,7 @@ interface Message { interface ComplianceAdvisorWidgetProps { currentStep?: string + enableDraftingEngine?: boolean } // ============================================================================= @@ -58,7 +59,13 @@ const EXAMPLE_QUESTIONS: Record = { // COMPONENT // ============================================================================= -export function ComplianceAdvisorWidget({ currentStep = 'default' }: ComplianceAdvisorWidgetProps) { +export function ComplianceAdvisorWidget({ currentStep = 'default', enableDraftingEngine = false }: ComplianceAdvisorWidgetProps) { + // Feature-flag: If Drafting Engine enabled, render DraftingEngineWidget instead + if (enableDraftingEngine) { + const { DraftingEngineWidget } = require('./DraftingEngineWidget') + return + } + const [isOpen, setIsOpen] = useState(false) const [isExpanded, setIsExpanded] = useState(false) const [messages, setMessages] = useState([]) diff --git a/admin-v2/components/sdk/DraftEditor.tsx b/admin-v2/components/sdk/DraftEditor.tsx new file mode 100644 index 0000000..d459780 --- /dev/null +++ b/admin-v2/components/sdk/DraftEditor.tsx @@ -0,0 +1,300 @@ +'use client' + +/** + * DraftEditor - Split-Pane Editor fuer Compliance-Dokument-Entwuerfe + * + * Links (2/3): Gerenderter Draft mit Section-Headern + * Rechts (1/3): Chat-Panel fuer iterative Verfeinerung + * Oben: Document-Type Label, Depth-Level Badge, Constraint-Compliance + */ + +import { useState, useRef, useCallback } from 'react' +import { DOCUMENT_TYPE_LABELS } from '@/lib/sdk/compliance-scope-types' +import type { ScopeDocumentType } from '@/lib/sdk/compliance-scope-types' +import type { + DraftRevision, + ConstraintCheckResult, + ValidationResult, +} from '@/lib/sdk/drafting-engine/types' + +interface DraftEditorProps { + draft: DraftRevision + documentType: ScopeDocumentType | null + constraintCheck: ConstraintCheckResult | null + validationResult: ValidationResult | null + isTyping: boolean + onAccept: () => void + onValidate: () => void + onClose: () => void + onRefine: (instruction: string) => void +} + +export function DraftEditor({ + draft, + documentType, + constraintCheck, + validationResult, + isTyping, + onAccept, + onValidate, + onClose, + onRefine, +}: DraftEditorProps) { + const [refineInput, setRefineInput] = useState('') + const [activeSection, setActiveSection] = useState(null) + const contentRef = useRef(null) + + const handleRefine = useCallback(() => { + if (!refineInput.trim() || isTyping) return + onRefine(refineInput.trim()) + setRefineInput('') + }, [refineInput, isTyping, onRefine]) + + const handleRefineKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault() + handleRefine() + } + } + + const docLabel = documentType + ? DOCUMENT_TYPE_LABELS[documentType]?.split(' (')[0] || documentType + : 'Dokument' + + return ( +
+
+ {/* Header */} +
+
+ + + +
+
{docLabel} - Entwurf
+
+ {draft.sections.length} Sections | Erstellt {new Date(draft.createdAt).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })} +
+
+
+ +
+ {/* Constraint Badge */} + {constraintCheck && ( + + {constraintCheck.allowed ? 'Constraints OK' : 'Constraint-Verletzung'} + + )} + + {/* Validation Badge */} + {validationResult && ( + + {validationResult.passed ? 'Validiert' : `${validationResult.errors.length} Fehler`} + + )} + + +
+
+ + {/* Adjustment Warnings */} + {constraintCheck && constraintCheck.adjustments.length > 0 && ( +
+ {constraintCheck.adjustments.map((adj, i) => ( +

+ + + + {adj} +

+ ))} +
+ )} + + {/* Main Content: 2/3 Editor + 1/3 Chat */} +
+ {/* Left: Draft Content (2/3) */} +
+ {/* Section Navigation */} +
+ {draft.sections.map((section) => ( + + ))} +
+ + {/* Sections */} +
+ {draft.sections.map((section) => ( +
+
+

{section.title}

+ {section.schemaField && ( + {section.schemaField} + )} +
+
+
+ {section.content} +
+
+
+ ))} +
+
+ + {/* Right: Refinement Chat (1/3) */} +
+
+

Verfeinerung

+

Geben Sie Anweisungen zur Verbesserung

+
+ + {/* Validation Summary (if present) */} + {validationResult && ( +
+
+ {validationResult.errors.length > 0 && ( +
+ + {validationResult.errors.length} Fehler +
+ )} + {validationResult.warnings.length > 0 && ( +
+ + {validationResult.warnings.length} Warnungen +
+ )} + {validationResult.suggestions.length > 0 && ( +
+ + {validationResult.suggestions.length} Vorschlaege +
+ )} +
+
+ )} + + {/* Refinement Area */} +
+
+

+ Beschreiben Sie, was geaendert werden soll. Der Agent erstellt eine ueberarbeitete Version unter Beachtung der Scope-Constraints. +

+ + {/* Quick Refinement Buttons */} +
+ {[ + 'Mehr Details hinzufuegen', + 'Platzhalter ausfuellen', + 'Rechtliche Referenzen ergaenzen', + 'Sprache vereinfachen', + ].map((suggestion) => ( + + ))} +
+
+
+ + {/* Refinement Input */} +
+
+ setRefineInput(e.target.value)} + onKeyDown={handleRefineKeyDown} + placeholder="Anweisung eingeben..." + disabled={isTyping} + className="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:opacity-50" + /> + +
+
+
+
+ + {/* Footer Actions */} +
+
+ +
+ +
+ + +
+
+
+
+ ) +} diff --git a/admin-v2/components/sdk/DraftingEngineWidget.tsx b/admin-v2/components/sdk/DraftingEngineWidget.tsx new file mode 100644 index 0000000..0079c6b --- /dev/null +++ b/admin-v2/components/sdk/DraftingEngineWidget.tsx @@ -0,0 +1,417 @@ +'use client' + +/** + * DraftingEngineWidget - Erweitert den ComplianceAdvisor um 4 Modi + * + * Mode-Indicator Pills: Explain / Ask / Draft / Validate + * Document-Type Selector aus requiredDocuments der ScopeDecision + * Feature-Flag enableDraftingEngine fuer schrittweises Rollout + */ + +import { useState, useEffect, useRef, useCallback } from 'react' +import { useSDK } from '@/lib/sdk/context' +import { useDraftingEngine } from '@/lib/sdk/drafting-engine/use-drafting-engine' +import { DOCUMENT_TYPE_LABELS } from '@/lib/sdk/compliance-scope-types' +import type { AgentMode } from '@/lib/sdk/drafting-engine/types' +import type { ScopeDocumentType } from '@/lib/sdk/compliance-scope-types' +import { DraftEditor } from './DraftEditor' +import { ValidationReport } from './ValidationReport' + +interface DraftingEngineWidgetProps { + currentStep?: string + enableDraftingEngine?: boolean +} + +const MODE_CONFIG: Record = { + explain: { label: 'Explain', color: 'bg-gray-100 text-gray-600', activeColor: 'bg-purple-100 text-purple-700 ring-1 ring-purple-300', icon: 'M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z' }, + ask: { label: 'Ask', color: 'bg-gray-100 text-gray-600', activeColor: 'bg-amber-100 text-amber-700 ring-1 ring-amber-300', icon: 'M8.228 9c.549-1.165 2.03-2 3.772-2 2.21 0 4 1.343 4 3 0 1.4-1.278 2.575-3.006 2.907-.542.104-.994.54-.994 1.093m0 3h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z' }, + draft: { label: 'Draft', color: 'bg-gray-100 text-gray-600', activeColor: 'bg-blue-100 text-blue-700 ring-1 ring-blue-300', icon: 'M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z' }, + validate: { label: 'Validate', color: 'bg-gray-100 text-gray-600', activeColor: 'bg-green-100 text-green-700 ring-1 ring-green-300', icon: 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z' }, +} + +const EXAMPLE_QUESTIONS: Record = { + explain: [ + 'Was ist ein Verarbeitungsverzeichnis?', + 'Wann brauche ich eine DSFA?', + 'Was sind TOM nach Art. 32 DSGVO?', + ], + ask: [ + 'Welche Luecken hat mein Compliance-Profil?', + 'Was fehlt noch fuer die Zertifizierung?', + 'Welche Dokumente muss ich noch erstellen?', + ], + draft: [ + 'Erstelle einen VVT-Eintrag fuer unseren Hauptprozess', + 'Erstelle TOM fuer unsere Cloud-Infrastruktur', + 'Erstelle eine Datenschutzerklaerung', + ], + validate: [ + 'Pruefe die Konsistenz meiner Dokumente', + 'Stimmen VVT und TOM ueberein?', + 'Gibt es Luecken bei den Loeschfristen?', + ], +} + +export function DraftingEngineWidget({ + currentStep = 'default', + enableDraftingEngine = true, +}: DraftingEngineWidgetProps) { + const { state } = useSDK() + const engine = useDraftingEngine() + const [isOpen, setIsOpen] = useState(false) + const [isExpanded, setIsExpanded] = useState(false) + const [inputValue, setInputValue] = useState('') + const [showDraftEditor, setShowDraftEditor] = useState(false) + const [showValidationReport, setShowValidationReport] = useState(false) + const messagesEndRef = useRef(null) + + // Available document types from scope decision + const availableDocumentTypes: ScopeDocumentType[] = + state.complianceScope?.decision?.requiredDocuments + ?.filter(d => d.required) + .map(d => d.documentType as ScopeDocumentType) ?? ['vvt', 'tom', 'lf'] + + // Auto-scroll + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }) + }, [engine.messages]) + + // Open draft editor when a new draft arrives + useEffect(() => { + if (engine.currentDraft) { + setShowDraftEditor(true) + } + }, [engine.currentDraft]) + + // Open validation report when new results arrive + useEffect(() => { + if (engine.validationResult) { + setShowValidationReport(true) + } + }, [engine.validationResult]) + + const handleSendMessage = useCallback( + (content: string) => { + if (!content.trim()) return + setInputValue('') + engine.sendMessage(content) + }, + [engine] + ) + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault() + handleSendMessage(inputValue) + } + } + + const exampleQuestions = EXAMPLE_QUESTIONS[engine.currentMode] + + if (!isOpen) { + return ( + + ) + } + + // Draft Editor full-screen overlay + if (showDraftEditor && engine.currentDraft) { + return ( + { + engine.acceptDraft() + setShowDraftEditor(false) + }} + onValidate={() => { + engine.validateDraft() + }} + onClose={() => setShowDraftEditor(false)} + onRefine={(instruction: string) => { + engine.requestDraft(instruction) + }} + validationResult={engine.validationResult} + isTyping={engine.isTyping} + /> + ) + } + + return ( +
+ {/* Header */} +
+
+
+ + + +
+
+
Drafting Engine
+
Compliance-Dokumententwurf
+
+
+
+ + +
+
+ + {/* Mode Pills */} +
+ {(Object.keys(MODE_CONFIG) as AgentMode[]).map((mode) => { + const config = MODE_CONFIG[mode] + const isActive = engine.currentMode === mode + return ( + + ) + })} +
+ + {/* Document Type Selector (visible in draft/validate mode) */} + {(engine.currentMode === 'draft' || engine.currentMode === 'validate') && ( +
+
+ Dokument: + +
+
+ )} + + {/* Error Banner */} + {engine.error && ( +
+ {engine.error} + +
+ )} + + {/* Validation Report Inline */} + {showValidationReport && engine.validationResult && ( +
+ setShowValidationReport(false)} + compact + /> +
+ )} + + {/* Messages Area */} +
+ {engine.messages.length === 0 ? ( +
+
+ + + +
+

+ {engine.currentMode === 'explain' && 'Fragen beantworten'} + {engine.currentMode === 'ask' && 'Luecken erkennen'} + {engine.currentMode === 'draft' && 'Dokumente entwerfen'} + {engine.currentMode === 'validate' && 'Konsistenz pruefen'} +

+

+ {engine.currentMode === 'explain' && 'Stellen Sie Fragen zu DSGVO, AI Act und Compliance.'} + {engine.currentMode === 'ask' && 'Identifiziert Luecken in Ihrem Compliance-Profil.'} + {engine.currentMode === 'draft' && 'Erstellt strukturierte Compliance-Dokumente.'} + {engine.currentMode === 'validate' && 'Prueft Cross-Dokument-Konsistenz.'} +

+ +
+

Beispiele:

+ {exampleQuestions.map((q, idx) => ( + + ))} +
+ + {/* Quick Actions for Draft/Validate */} + {engine.currentMode === 'draft' && engine.activeDocumentType && ( + + )} + {engine.currentMode === 'validate' && ( + + )} +
+ ) : ( + <> + {engine.messages.map((message, idx) => ( +
+
+

+ {message.content} +

+ + {/* Draft ready indicator */} + {message.metadata?.hasDraft && engine.currentDraft && ( + + )} + + {/* Validation ready indicator */} + {message.metadata?.hasValidation && engine.validationResult && ( + + )} +
+
+ ))} + + {engine.isTyping && ( +
+
+
+
+
+
+
+
+
+ )} + +
+ + )} +
+ + {/* Input Area */} +
+
+ setInputValue(e.target.value)} + onKeyDown={handleKeyDown} + placeholder={ + engine.currentMode === 'draft' + ? 'Anweisung fuer den Entwurf...' + : engine.currentMode === 'validate' + ? 'Validierungsfrage...' + : 'Frage eingeben...' + } + disabled={engine.isTyping} + className="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-transparent disabled:opacity-50" + /> + {engine.isTyping ? ( + + ) : ( + + )} +
+
+
+ ) +} diff --git a/admin-v2/components/sdk/ValidationReport.tsx b/admin-v2/components/sdk/ValidationReport.tsx new file mode 100644 index 0000000..abe4a79 --- /dev/null +++ b/admin-v2/components/sdk/ValidationReport.tsx @@ -0,0 +1,220 @@ +'use client' + +/** + * ValidationReport - Strukturierte Anzeige von Validierungsergebnissen + * + * Errors (Scope-Violations) in Rot + * Warnings (Inkonsistenzen) in Amber + * Suggestions in Blau + */ + +import { DOCUMENT_TYPE_LABELS } from '@/lib/sdk/compliance-scope-types' +import type { ValidationResult, ValidationFinding } from '@/lib/sdk/drafting-engine/types' + +interface ValidationReportProps { + result: ValidationResult + onClose: () => void + /** Compact mode for inline display in widget */ + compact?: boolean +} + +const SEVERITY_CONFIG = { + error: { + bg: 'bg-red-50', + border: 'border-red-200', + text: 'text-red-700', + icon: 'M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z', + label: 'Fehler', + dotColor: 'bg-red-500', + }, + warning: { + bg: 'bg-amber-50', + border: 'border-amber-200', + text: 'text-amber-700', + icon: 'M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z', + label: 'Warnungen', + dotColor: 'bg-amber-500', + }, + suggestion: { + bg: 'bg-blue-50', + border: 'border-blue-200', + text: 'text-blue-700', + icon: 'M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z', + label: 'Vorschlaege', + dotColor: 'bg-blue-500', + }, +} + +function FindingCard({ finding, compact }: { finding: ValidationFinding; compact?: boolean }) { + const config = SEVERITY_CONFIG[finding.severity] + const docLabel = DOCUMENT_TYPE_LABELS[finding.documentType]?.split(' (')[0] || finding.documentType + + if (compact) { + return ( +
+ +
+

{finding.title}

+

{finding.description}

+
+
+ ) + } + + return ( +
+
+ + + +
+
+

{finding.title}

+ {docLabel} +
+

{finding.description}

+ + {finding.crossReferenceType && ( +

+ Cross-Referenz: {DOCUMENT_TYPE_LABELS[finding.crossReferenceType]?.split(' (')[0] || finding.crossReferenceType} +

+ )} + + {finding.legalReference && ( +

{finding.legalReference}

+ )} + + {finding.suggestion && ( +
+ + + +

{finding.suggestion}

+
+ )} +
+
+
+ ) +} + +export function ValidationReport({ result, onClose, compact }: ValidationReportProps) { + const totalFindings = result.errors.length + result.warnings.length + result.suggestions.length + + if (compact) { + return ( +
+
+
+ + + {result.passed ? 'Validierung bestanden' : 'Validierung fehlgeschlagen'} + + + ({totalFindings} {totalFindings === 1 ? 'Fund' : 'Funde'}) + +
+ +
+
+ {result.errors.map((f) => )} + {result.warnings.map((f) => )} + {result.suggestions.map((f) => )} +
+
+ ) + } + + return ( +
+ {/* Summary Header */} +
+
+
+
+ + + +
+
+

+ {result.passed ? 'Validierung bestanden' : 'Validierung fehlgeschlagen'} +

+

+ Level {result.scopeLevel} | {new Date(result.timestamp).toLocaleString('de-DE')} +

+
+
+ + {/* Stats */} +
+ {result.errors.length > 0 && ( +
+ + {result.errors.length} +
+ )} + {result.warnings.length > 0 && ( +
+ + {result.warnings.length} +
+ )} + {result.suggestions.length > 0 && ( +
+ + {result.suggestions.length} +
+ )} + + +
+
+
+ + {/* Errors */} + {result.errors.length > 0 && ( +
+

+ Fehler ({result.errors.length}) +

+
+ {result.errors.map((f) => )} +
+
+ )} + + {/* Warnings */} + {result.warnings.length > 0 && ( +
+

+ Warnungen ({result.warnings.length}) +

+
+ {result.warnings.map((f) => )} +
+
+ )} + + {/* Suggestions */} + {result.suggestions.length > 0 && ( +
+

+ Vorschlaege ({result.suggestions.length}) +

+
+ {result.suggestions.map((f) => )} +
+
+ )} +
+ ) +} diff --git a/admin-v2/lib/sdk/drafting-engine/__tests__/constraint-enforcer.test.ts b/admin-v2/lib/sdk/drafting-engine/__tests__/constraint-enforcer.test.ts new file mode 100644 index 0000000..929c40c --- /dev/null +++ b/admin-v2/lib/sdk/drafting-engine/__tests__/constraint-enforcer.test.ts @@ -0,0 +1,224 @@ +import { ConstraintEnforcer } from '../constraint-enforcer' +import type { ScopeDecision } from '../../compliance-scope-types' + +describe('ConstraintEnforcer', () => { + const enforcer = new ConstraintEnforcer() + + // Helper: minimal valid ScopeDecision + function makeDecision(overrides: Partial = {}): ScopeDecision { + return { + id: 'test-decision', + determinedLevel: 'L2', + scores: { risk_score: 50, complexity_score: 50, assurance_need: 50, composite_score: 50 }, + triggeredHardTriggers: [], + requiredDocuments: [ + { documentType: 'vvt', label: 'VVT', required: true, depth: 'Standard', detailItems: [], estimatedEffort: '2h', triggeredBy: [] }, + { documentType: 'tom', label: 'TOM', required: true, depth: 'Standard', detailItems: [], estimatedEffort: '3h', triggeredBy: [] }, + { documentType: 'lf', label: 'LF', required: true, depth: 'Basis', detailItems: [], estimatedEffort: '1h', triggeredBy: [] }, + ], + riskFlags: [], + gaps: [], + nextActions: [], + reasoning: [], + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + ...overrides, + } as ScopeDecision + } + + describe('check - no decision', () => { + it('should allow basic documents (vvt, tom, dsi) without decision', () => { + const result = enforcer.check('vvt', null) + expect(result.allowed).toBe(true) + expect(result.adjustments.length).toBeGreaterThan(0) + expect(result.checkedRules).toContain('RULE-NO-DECISION') + }) + + it('should allow tom without decision', () => { + const result = enforcer.check('tom', null) + expect(result.allowed).toBe(true) + }) + + it('should allow dsi without decision', () => { + const result = enforcer.check('dsi', null) + expect(result.allowed).toBe(true) + }) + + it('should block non-basic documents without decision', () => { + const result = enforcer.check('dsfa', null) + expect(result.allowed).toBe(false) + expect(result.violations.length).toBeGreaterThan(0) + }) + + it('should block av_vertrag without decision', () => { + const result = enforcer.check('av_vertrag', null) + expect(result.allowed).toBe(false) + }) + }) + + describe('check - RULE-DOC-REQUIRED', () => { + it('should allow required documents', () => { + const decision = makeDecision() + const result = enforcer.check('vvt', decision) + expect(result.allowed).toBe(true) + }) + + it('should warn but allow optional documents', () => { + const decision = makeDecision({ + requiredDocuments: [ + { documentType: 'vvt', label: 'VVT', required: true, depth: 'Standard', detailItems: [], estimatedEffort: '2h', triggeredBy: [] }, + ], + }) + const result = enforcer.check('dsfa', decision) + expect(result.allowed).toBe(true) // Only warns, does not block + expect(result.adjustments.some(a => a.includes('nicht als Pflicht'))).toBe(true) + }) + }) + + describe('check - RULE-DEPTH-MATCH', () => { + it('should block when requested depth exceeds determined level', () => { + const decision = makeDecision({ determinedLevel: 'L2' }) + const result = enforcer.check('vvt', decision, 'L4') + expect(result.allowed).toBe(false) + expect(result.violations.some(v => v.includes('ueberschreitet'))).toBe(true) + }) + + it('should allow when requested depth matches level', () => { + const decision = makeDecision({ determinedLevel: 'L2' }) + const result = enforcer.check('vvt', decision, 'L2') + expect(result.allowed).toBe(true) + }) + + it('should adjust when requested depth is below level', () => { + const decision = makeDecision({ determinedLevel: 'L3' }) + const result = enforcer.check('vvt', decision, 'L1') + expect(result.allowed).toBe(true) + expect(result.adjustments.some(a => a.includes('angehoben'))).toBe(true) + }) + + it('should allow without requested depth level', () => { + const decision = makeDecision({ determinedLevel: 'L3' }) + const result = enforcer.check('vvt', decision) + expect(result.allowed).toBe(true) + }) + }) + + describe('check - RULE-DSFA-ENFORCEMENT', () => { + it('should note when DSFA is not required but requested', () => { + const decision = makeDecision({ determinedLevel: 'L2' }) + const result = enforcer.check('dsfa', decision) + expect(result.allowed).toBe(true) + expect(result.adjustments.some(a => a.includes('nicht verpflichtend'))).toBe(true) + }) + + it('should allow DSFA when hard triggers require it', () => { + const decision = makeDecision({ + determinedLevel: 'L3', + triggeredHardTriggers: [{ + rule: { + id: 'HT-ART9', + label: 'Art. 9 Daten', + description: '', + conditionField: '', + conditionOperator: 'EQUALS' as const, + conditionValue: null, + minimumLevel: 'L3', + mandatoryDocuments: ['dsfa'], + dsfaRequired: true, + legalReference: 'Art. 35 DSGVO', + }, + matchedValue: null, + explanation: 'Art. 9 Daten verarbeitet', + }], + }) + const result = enforcer.check('dsfa', decision) + expect(result.allowed).toBe(true) + }) + + it('should warn about DSFA when drafting non-DSFA but DSFA is required', () => { + const decision = makeDecision({ + determinedLevel: 'L3', + triggeredHardTriggers: [{ + rule: { + id: 'HT-ART9', + label: 'Art. 9 Daten', + description: '', + conditionField: '', + conditionOperator: 'EQUALS' as const, + conditionValue: null, + minimumLevel: 'L3', + mandatoryDocuments: ['dsfa'], + dsfaRequired: true, + legalReference: 'Art. 35 DSGVO', + }, + matchedValue: null, + explanation: '', + }], + requiredDocuments: [ + { documentType: 'dsfa', label: 'DSFA', required: true, depth: 'Vollstaendig', detailItems: [], estimatedEffort: '8h', triggeredBy: [] }, + { documentType: 'vvt', label: 'VVT', required: true, depth: 'Standard', detailItems: [], estimatedEffort: '2h', triggeredBy: [] }, + ], + }) + const result = enforcer.check('vvt', decision) + expect(result.allowed).toBe(true) + expect(result.adjustments.some(a => a.includes('DSFA') && a.includes('verpflichtend'))).toBe(true) + }) + }) + + describe('check - RULE-RISK-FLAGS', () => { + it('should note critical risk flags', () => { + const decision = makeDecision({ + riskFlags: [ + { id: 'rf-1', severity: 'CRITICAL', title: 'Offene Art. 9 Verarbeitung', description: '', recommendation: 'DSFA durchfuehren' }, + { id: 'rf-2', severity: 'HIGH', title: 'Fehlende Verschluesselung', description: '', recommendation: 'TOM erstellen' }, + { id: 'rf-3', severity: 'LOW', title: 'Dokumentation unvollstaendig', description: '', recommendation: '' }, + ], + }) + const result = enforcer.check('vvt', decision) + expect(result.allowed).toBe(true) + expect(result.adjustments.some(a => a.includes('2 kritische/hohe Risiko-Flags'))).toBe(true) + }) + + it('should not flag when no risk flags present', () => { + const decision = makeDecision({ riskFlags: [] }) + const result = enforcer.check('vvt', decision) + expect(result.adjustments.every(a => !a.includes('Risiko-Flags'))).toBe(true) + }) + }) + + describe('check - checkedRules tracking', () => { + it('should track all checked rules', () => { + const decision = makeDecision() + const result = enforcer.check('vvt', decision) + expect(result.checkedRules).toContain('RULE-DOC-REQUIRED') + expect(result.checkedRules).toContain('RULE-DEPTH-MATCH') + expect(result.checkedRules).toContain('RULE-DSFA-ENFORCEMENT') + expect(result.checkedRules).toContain('RULE-RISK-FLAGS') + expect(result.checkedRules).toContain('RULE-HARD-TRIGGER-CONSISTENCY') + }) + }) + + describe('checkFromContext', () => { + it('should reconstruct decision from DraftContext and check', () => { + const context = { + decisions: { + level: 'L2' as const, + scores: { risk_score: 50, complexity_score: 50, assurance_need: 50, composite_score: 50 }, + hardTriggers: [], + requiredDocuments: [ + { documentType: 'vvt' as const, depth: 'Standard', detailItems: [] }, + ], + }, + companyProfile: { name: 'Test GmbH', industry: 'IT', employeeCount: 50, businessModel: 'SaaS', isPublicSector: false }, + constraints: { + depthRequirements: { required: true, depth: 'Standard', detailItems: [], estimatedEffort: '2h' }, + riskFlags: [], + boundaries: [], + }, + } + const result = enforcer.checkFromContext('vvt', context) + expect(result.allowed).toBe(true) + expect(result.checkedRules.length).toBeGreaterThan(0) + }) + }) +}) diff --git a/admin-v2/lib/sdk/drafting-engine/__tests__/intent-classifier.test.ts b/admin-v2/lib/sdk/drafting-engine/__tests__/intent-classifier.test.ts new file mode 100644 index 0000000..521ea1c --- /dev/null +++ b/admin-v2/lib/sdk/drafting-engine/__tests__/intent-classifier.test.ts @@ -0,0 +1,153 @@ +import { IntentClassifier } from '../intent-classifier' + +describe('IntentClassifier', () => { + const classifier = new IntentClassifier() + + describe('classify - Draft mode', () => { + it.each([ + ['Erstelle ein VVT fuer unseren Hauptprozess', 'draft'], + ['Generiere eine TOM-Dokumentation', 'draft'], + ['Schreibe eine Datenschutzerklaerung', 'draft'], + ['Verfasse einen Entwurf fuer das Loeschkonzept', 'draft'], + ['Create a DSFA document', 'draft'], + ['Draft a privacy policy for us', 'draft'], + ['Neues VVT anlegen', 'draft'], + ])('"%s" should classify as %s', (input, expectedMode) => { + const result = classifier.classify(input) + expect(result.mode).toBe(expectedMode) + expect(result.confidence).toBeGreaterThan(0.7) + }) + }) + + describe('classify - Validate mode', () => { + it.each([ + ['Pruefe die Konsistenz meiner Dokumente', 'validate'], + ['Ist mein VVT korrekt?', 'validate'], + ['Validiere die TOM gegen das VVT', 'validate'], + ['Check die Vollstaendigkeit', 'validate'], + ['Stimmt das mit der DSFA ueberein?', 'validate'], + ['Cross-Check VVT und TOM', 'validate'], + ])('"%s" should classify as %s', (input, expectedMode) => { + const result = classifier.classify(input) + expect(result.mode).toBe(expectedMode) + expect(result.confidence).toBeGreaterThan(0.7) + }) + }) + + describe('classify - Ask mode', () => { + it.each([ + ['Was fehlt noch in meinem Profil?', 'ask'], + ['Zeige mir die Luecken', 'ask'], + ['Welche Dokumente fehlen noch?', 'ask'], + ['Was ist der naechste Schritt?', 'ask'], + ['Welche Informationen brauche ich noch?', 'ask'], + ])('"%s" should classify as %s', (input, expectedMode) => { + const result = classifier.classify(input) + expect(result.mode).toBe(expectedMode) + expect(result.confidence).toBeGreaterThan(0.6) + }) + }) + + describe('classify - Explain mode (fallback)', () => { + it.each([ + ['Was ist DSGVO?', 'explain'], + ['Erklaere mir Art. 30', 'explain'], + ['Hallo', 'explain'], + ['Danke fuer die Hilfe', 'explain'], + ])('"%s" should classify as %s (fallback)', (input, expectedMode) => { + const result = classifier.classify(input) + expect(result.mode).toBe(expectedMode) + }) + }) + + describe('classify - confidence thresholds', () => { + it('should have high confidence for clear draft intents', () => { + const result = classifier.classify('Erstelle ein neues VVT') + expect(result.confidence).toBeGreaterThanOrEqual(0.85) + }) + + it('should have lower confidence for ambiguous inputs', () => { + const result = classifier.classify('Hallo') + expect(result.confidence).toBeLessThan(0.6) + }) + + it('should boost confidence with document type detection', () => { + const withDoc = classifier.classify('Erstelle VVT') + const withoutDoc = classifier.classify('Erstelle etwas') + expect(withDoc.confidence).toBeGreaterThanOrEqual(withoutDoc.confidence) + }) + + it('should boost confidence with multiple pattern matches', () => { + const single = classifier.classify('Erstelle Dokument') + const multi = classifier.classify('Erstelle und generiere ein neues Dokument') + expect(multi.confidence).toBeGreaterThanOrEqual(single.confidence) + }) + }) + + describe('detectDocumentType', () => { + it.each([ + ['VVT erstellen', 'vvt'], + ['Verarbeitungsverzeichnis', 'vvt'], + ['Art. 30 Dokumentation', 'vvt'], + ['TOM definieren', 'tom'], + ['technisch organisatorische Massnahmen', 'tom'], + ['Art. 32 Massnahmen', 'tom'], + ['DSFA durchfuehren', 'dsfa'], + ['Datenschutz-Folgenabschaetzung', 'dsfa'], + ['Art. 35 Pruefung', 'dsfa'], + ['DPIA erstellen', 'dsfa'], + ['Datenschutzerklaerung', 'dsi'], + ['Privacy Policy', 'dsi'], + ['Art. 13 Information', 'dsi'], + ['Loeschfristen definieren', 'lf'], + ['Loeschkonzept erstellen', 'lf'], + ['Retention Policy', 'lf'], + ['Auftragsverarbeitung', 'av_vertrag'], + ['AVV erstellen', 'av_vertrag'], + ['Art. 28 Vertrag', 'av_vertrag'], + ['Einwilligung einholen', 'einwilligung'], + ['Consent Management', 'einwilligung'], + ['Cookie Banner', 'einwilligung'], + ])('"%s" should detect document type %s', (input, expectedType) => { + const result = classifier.detectDocumentType(input) + expect(result).toBe(expectedType) + }) + + it('should return undefined for unrecognized types', () => { + expect(classifier.detectDocumentType('Hallo Welt')).toBeUndefined() + expect(classifier.detectDocumentType('Was kostet das?')).toBeUndefined() + }) + }) + + describe('classify - Umlaut handling', () => { + it('should handle German umlauts correctly', () => { + // With actual umlauts (ä, ö, ü) + const result1 = classifier.classify('Prüfe die Vollständigkeit') + expect(result1.mode).toBe('validate') + + // With ae/oe/ue substitution + const result2 = classifier.classify('Pruefe die Vollstaendigkeit') + expect(result2.mode).toBe('validate') + }) + + it('should handle ß correctly', () => { + const result = classifier.classify('Schließe Lücken') + // Should still detect via normalized patterns + expect(result).toBeDefined() + }) + }) + + describe('classify - combined mode + document type', () => { + it('should detect both mode and document type', () => { + const result = classifier.classify('Erstelle ein VVT fuer unsere Firma') + expect(result.mode).toBe('draft') + expect(result.detectedDocumentType).toBe('vvt') + }) + + it('should detect validate + document type', () => { + const result = classifier.classify('Pruefe mein TOM auf Konsistenz') + expect(result.mode).toBe('validate') + expect(result.detectedDocumentType).toBe('tom') + }) + }) +}) diff --git a/admin-v2/lib/sdk/drafting-engine/__tests__/state-projector.test.ts b/admin-v2/lib/sdk/drafting-engine/__tests__/state-projector.test.ts new file mode 100644 index 0000000..20425f6 --- /dev/null +++ b/admin-v2/lib/sdk/drafting-engine/__tests__/state-projector.test.ts @@ -0,0 +1,311 @@ +import { StateProjector } from '../state-projector' +import type { SDKState } from '../../types' + +describe('StateProjector', () => { + const projector = new StateProjector() + + // Helper: minimal SDKState + function makeState(overrides: Partial = {}): SDKState { + return { + version: '1.0.0', + lastModified: new Date(), + tenantId: 'test', + userId: 'user1', + subscription: 'PROFESSIONAL', + customerType: null, + companyProfile: null, + complianceScope: null, + currentPhase: 1, + currentStep: 'company-profile', + completedSteps: [], + checkpoints: {}, + importedDocuments: [], + gapAnalysis: null, + useCases: [], + activeUseCase: null, + screening: null, + modules: [], + requirements: [], + controls: [], + evidence: [], + checklist: [], + risks: [], + aiActClassification: null, + obligations: [], + dsfa: null, + toms: [], + retentionPolicies: [], + vvt: [], + documents: [], + cookieBanner: null, + consents: [], + dsrConfig: null, + escalationWorkflows: [], + preferences: { + language: 'de', + theme: 'light', + compactMode: false, + showHints: true, + autoSave: true, + autoValidate: true, + allowParallelWork: true, + }, + ...overrides, + } as SDKState + } + + function makeDecisionState(level: string = 'L2'): SDKState { + return makeState({ + companyProfile: { + companyName: 'Test GmbH', + industry: 'IT-Dienstleistung', + employeeCount: 50, + businessModel: 'SaaS', + isPublicSector: false, + } as any, + complianceScope: { + decision: { + id: 'dec-1', + determinedLevel: level, + scores: { risk_score: 60, complexity_score: 50, assurance_need: 55, composite_score: 55 }, + triggeredHardTriggers: [], + requiredDocuments: [ + { documentType: 'vvt', label: 'VVT', required: true, depth: 'Standard', detailItems: ['Bezeichnung', 'Zweck'], estimatedEffort: '2h', triggeredBy: [] }, + { documentType: 'tom', label: 'TOM', required: true, depth: 'Standard', detailItems: ['Verschluesselung'], estimatedEffort: '3h', triggeredBy: [] }, + { documentType: 'lf', label: 'LF', required: true, depth: 'Basis', detailItems: [], estimatedEffort: '1h', triggeredBy: [] }, + ], + riskFlags: [ + { id: 'rf-1', severity: 'MEDIUM', title: 'Cloud-Nutzung', description: '', recommendation: 'AVV pruefen' }, + ], + gaps: [ + { id: 'gap-1', severity: 'high', title: 'TOM fehlt', description: 'Keine TOM definiert', relatedDocuments: ['tom'] }, + ], + nextActions: [], + reasoning: [], + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, + answers: [], + } as any, + vvt: [{ id: 'vvt-1', name: 'Kundenverwaltung' }] as any[], + toms: [], + retentionPolicies: [], + }) + } + + describe('projectForDraft', () => { + it('should return a DraftContext with correct structure', () => { + const state = makeDecisionState() + const result = projector.projectForDraft(state, 'vvt') + + expect(result).toHaveProperty('decisions') + expect(result).toHaveProperty('companyProfile') + expect(result).toHaveProperty('constraints') + expect(result.decisions.level).toBe('L2') + }) + + it('should project company profile', () => { + const state = makeDecisionState() + const result = projector.projectForDraft(state, 'vvt') + + expect(result.companyProfile.name).toBe('Test GmbH') + expect(result.companyProfile.industry).toBe('IT-Dienstleistung') + expect(result.companyProfile.employeeCount).toBe(50) + }) + + it('should provide defaults when no company profile', () => { + const state = makeState() + const result = projector.projectForDraft(state, 'vvt') + + expect(result.companyProfile.name).toBe('Unbekannt') + expect(result.companyProfile.industry).toBe('Unbekannt') + expect(result.companyProfile.employeeCount).toBe(0) + }) + + it('should extract constraints and depth requirements', () => { + const state = makeDecisionState() + const result = projector.projectForDraft(state, 'vvt') + + expect(result.constraints.depthRequirements).toBeDefined() + expect(result.constraints.boundaries.length).toBeGreaterThan(0) + }) + + it('should extract risk flags', () => { + const state = makeDecisionState() + const result = projector.projectForDraft(state, 'vvt') + + expect(result.constraints.riskFlags.length).toBe(1) + expect(result.constraints.riskFlags[0].title).toBe('Cloud-Nutzung') + }) + + it('should include existing document data when available', () => { + const state = makeDecisionState() + const result = projector.projectForDraft(state, 'vvt') + + expect(result.existingDocumentData).toBeDefined() + expect((result.existingDocumentData as any).totalCount).toBe(1) + }) + + it('should return undefined existingDocumentData when none exists', () => { + const state = makeDecisionState() + const result = projector.projectForDraft(state, 'tom') + + expect(result.existingDocumentData).toBeUndefined() + }) + + it('should filter required documents', () => { + const state = makeDecisionState() + const result = projector.projectForDraft(state, 'vvt') + + expect(result.decisions.requiredDocuments.length).toBe(3) + expect(result.decisions.requiredDocuments.every(d => d.documentType)).toBe(true) + }) + + it('should handle empty state gracefully', () => { + const state = makeState() + const result = projector.projectForDraft(state, 'vvt') + + expect(result.decisions.level).toBe('L1') + expect(result.decisions.hardTriggers).toEqual([]) + expect(result.decisions.requiredDocuments).toEqual([]) + }) + }) + + describe('projectForAsk', () => { + it('should return a GapContext with correct structure', () => { + const state = makeDecisionState() + const result = projector.projectForAsk(state) + + expect(result).toHaveProperty('unansweredQuestions') + expect(result).toHaveProperty('gaps') + expect(result).toHaveProperty('missingDocuments') + }) + + it('should identify missing documents', () => { + const state = makeDecisionState() + // vvt exists, tom and lf are missing + const result = projector.projectForAsk(state) + + expect(result.missingDocuments.some(d => d.documentType === 'tom')).toBe(true) + expect(result.missingDocuments.some(d => d.documentType === 'lf')).toBe(true) + }) + + it('should not list existing documents as missing', () => { + const state = makeDecisionState() + const result = projector.projectForAsk(state) + + // vvt exists in state + expect(result.missingDocuments.some(d => d.documentType === 'vvt')).toBe(false) + }) + + it('should include gaps from scope decision', () => { + const state = makeDecisionState() + const result = projector.projectForAsk(state) + + expect(result.gaps.length).toBe(1) + expect(result.gaps[0].title).toBe('TOM fehlt') + }) + + it('should handle empty state', () => { + const state = makeState() + const result = projector.projectForAsk(state) + + expect(result.gaps).toEqual([]) + expect(result.missingDocuments).toEqual([]) + }) + }) + + describe('projectForValidate', () => { + it('should return a ValidationContext with correct structure', () => { + const state = makeDecisionState() + const result = projector.projectForValidate(state, ['vvt', 'tom', 'lf']) + + expect(result).toHaveProperty('documents') + expect(result).toHaveProperty('crossReferences') + expect(result).toHaveProperty('scopeLevel') + expect(result).toHaveProperty('depthRequirements') + }) + + it('should include all requested document types', () => { + const state = makeDecisionState() + const result = projector.projectForValidate(state, ['vvt', 'tom']) + + expect(result.documents.length).toBe(2) + expect(result.documents.map(d => d.type)).toContain('vvt') + expect(result.documents.map(d => d.type)).toContain('tom') + }) + + it('should include cross-references', () => { + const state = makeDecisionState() + const result = projector.projectForValidate(state, ['vvt', 'tom', 'lf']) + + expect(result.crossReferences).toHaveProperty('vvtCategories') + expect(result.crossReferences).toHaveProperty('tomControls') + expect(result.crossReferences).toHaveProperty('retentionCategories') + expect(result.crossReferences.vvtCategories.length).toBe(1) + expect(result.crossReferences.vvtCategories[0]).toBe('Kundenverwaltung') + }) + + it('should include scope level', () => { + const state = makeDecisionState('L3') + const result = projector.projectForValidate(state, ['vvt']) + + expect(result.scopeLevel).toBe('L3') + }) + + it('should include depth requirements per document type', () => { + const state = makeDecisionState() + const result = projector.projectForValidate(state, ['vvt', 'tom']) + + expect(result.depthRequirements).toHaveProperty('vvt') + expect(result.depthRequirements).toHaveProperty('tom') + }) + + it('should summarize documents', () => { + const state = makeDecisionState() + const result = projector.projectForValidate(state, ['vvt', 'tom']) + + expect(result.documents[0].contentSummary).toContain('1') + expect(result.documents[1].contentSummary).toContain('Keine TOM') + }) + + it('should handle empty state', () => { + const state = makeState() + const result = projector.projectForValidate(state, ['vvt', 'tom', 'lf']) + + expect(result.scopeLevel).toBe('L1') + expect(result.crossReferences.vvtCategories).toEqual([]) + expect(result.crossReferences.tomControls).toEqual([]) + }) + }) + + describe('token budget estimation', () => { + it('projectForDraft should produce compact output', () => { + const state = makeDecisionState() + const result = projector.projectForDraft(state, 'vvt') + const json = JSON.stringify(result) + + // Rough token estimation: ~4 chars per token + const estimatedTokens = json.length / 4 + expect(estimatedTokens).toBeLessThan(2000) // Budget is ~1500 + }) + + it('projectForAsk should produce very compact output', () => { + const state = makeDecisionState() + const result = projector.projectForAsk(state) + const json = JSON.stringify(result) + + const estimatedTokens = json.length / 4 + expect(estimatedTokens).toBeLessThan(1000) // Budget is ~600 + }) + + it('projectForValidate should stay within budget', () => { + const state = makeDecisionState() + const result = projector.projectForValidate(state, ['vvt', 'tom', 'lf']) + const json = JSON.stringify(result) + + const estimatedTokens = json.length / 4 + expect(estimatedTokens).toBeLessThan(3000) // Budget is ~2000 + }) + }) +}) diff --git a/admin-v2/lib/sdk/drafting-engine/constraint-enforcer.ts b/admin-v2/lib/sdk/drafting-engine/constraint-enforcer.ts new file mode 100644 index 0000000..dace188 --- /dev/null +++ b/admin-v2/lib/sdk/drafting-engine/constraint-enforcer.ts @@ -0,0 +1,221 @@ +/** + * Constraint Enforcer - Hard Gate vor jedem Draft + * + * Stellt sicher, dass die Drafting Engine NIEMALS die deterministische + * Scope-Engine ueberschreibt. Prueft vor jedem Draft-Vorgang: + * + * 1. Ist der Dokumenttyp in requiredDocuments? + * 2. Passt die Draft-Tiefe zum Level? + * 3. Ist eine DSFA erforderlich (Hard Trigger)? + * 4. Werden Risiko-Flags beruecksichtigt? + */ + +import type { ScopeDecision, ScopeDocumentType, ComplianceDepthLevel } from '../compliance-scope-types' +import { DOCUMENT_SCOPE_MATRIX, getDepthLevelNumeric } from '../compliance-scope-types' +import type { ConstraintCheckResult, DraftContext } from './types' + +export class ConstraintEnforcer { + + /** + * Prueft ob ein Draft fuer den gegebenen Dokumenttyp erlaubt ist. + * Dies ist ein HARD GATE - bei Violation wird der Draft blockiert. + */ + check( + documentType: ScopeDocumentType, + decision: ScopeDecision | null, + requestedDepthLevel?: ComplianceDepthLevel + ): ConstraintCheckResult { + const violations: string[] = [] + const adjustments: string[] = [] + const checkedRules: string[] = [] + + // Wenn keine Decision vorhanden: Nur Basis-Drafts erlauben + if (!decision) { + checkedRules.push('RULE-NO-DECISION') + if (documentType !== 'vvt' && documentType !== 'tom' && documentType !== 'dsi') { + violations.push( + 'Scope-Evaluierung fehlt. Bitte zuerst das Compliance-Profiling durchfuehren.' + ) + } else { + adjustments.push( + 'Ohne Scope-Evaluierung wird Level L1 (Basis) angenommen.' + ) + } + return { + allowed: violations.length === 0, + violations, + adjustments, + checkedRules, + } + } + + const level = decision.determinedLevel + const levelNumeric = getDepthLevelNumeric(level) + + // ----------------------------------------------------------------------- + // Rule 1: Dokumenttyp in requiredDocuments? + // ----------------------------------------------------------------------- + checkedRules.push('RULE-DOC-REQUIRED') + const isRequired = decision.requiredDocuments.some( + d => d.documentType === documentType && d.required + ) + const scopeReq = DOCUMENT_SCOPE_MATRIX[documentType]?.[level] + + if (!isRequired && scopeReq && !scopeReq.required) { + // Nicht blockieren, aber warnen + adjustments.push( + `Dokument "${documentType}" ist auf Level ${level} nicht als Pflicht eingestuft. ` + + `Entwurf ist moeglich, aber optional.` + ) + } + + // ----------------------------------------------------------------------- + // Rule 2: Draft-Tiefe passt zum Level? + // ----------------------------------------------------------------------- + checkedRules.push('RULE-DEPTH-MATCH') + if (requestedDepthLevel) { + const requestedNumeric = getDepthLevelNumeric(requestedDepthLevel) + + if (requestedNumeric > levelNumeric) { + violations.push( + `Angefragte Tiefe ${requestedDepthLevel} ueberschreitet das bestimmte Level ${level}. ` + + `Die Scope-Engine hat Level ${level} festgelegt. ` + + `Ein Draft mit Tiefe ${requestedDepthLevel} ist nicht erlaubt.` + ) + } else if (requestedNumeric < levelNumeric) { + adjustments.push( + `Angefragte Tiefe ${requestedDepthLevel} liegt unter dem bestimmten Level ${level}. ` + + `Draft wird auf Level ${level} angehoben.` + ) + } + } + + // ----------------------------------------------------------------------- + // Rule 3: DSFA-Enforcement + // ----------------------------------------------------------------------- + checkedRules.push('RULE-DSFA-ENFORCEMENT') + if (documentType === 'dsfa') { + const dsfaRequired = decision.triggeredHardTriggers.some( + t => t.rule.dsfaRequired + ) + + if (!dsfaRequired && level !== 'L4') { + adjustments.push( + 'DSFA ist laut Scope-Engine nicht verpflichtend. ' + + 'Entwurf wird als freiwillige Massnahme gekennzeichnet.' + ) + } + } + + // Umgekehrt: Wenn DSFA verpflichtend und Typ != dsfa, ggf. hinweisen + if (documentType !== 'dsfa') { + const dsfaRequired = decision.triggeredHardTriggers.some( + t => t.rule.dsfaRequired + ) + const dsfaInRequired = decision.requiredDocuments.some( + d => d.documentType === 'dsfa' && d.required + ) + + if (dsfaRequired && dsfaInRequired) { + // Nur ein Hinweis, kein Block + adjustments.push( + 'Hinweis: Eine DSFA ist laut Scope-Engine verpflichtend. ' + + 'Bitte sicherstellen, dass auch eine DSFA erstellt wird.' + ) + } + } + + // ----------------------------------------------------------------------- + // Rule 4: Risiko-Flags beruecksichtigt? + // ----------------------------------------------------------------------- + checkedRules.push('RULE-RISK-FLAGS') + const criticalRisks = decision.riskFlags.filter( + f => f.severity === 'CRITICAL' || f.severity === 'HIGH' + ) + + if (criticalRisks.length > 0) { + adjustments.push( + `${criticalRisks.length} kritische/hohe Risiko-Flags erkannt. ` + + `Draft muss diese adressieren: ${criticalRisks.map(r => r.title).join(', ')}` + ) + } + + // ----------------------------------------------------------------------- + // Rule 5: Hard-Trigger Consistency + // ----------------------------------------------------------------------- + checkedRules.push('RULE-HARD-TRIGGER-CONSISTENCY') + for (const trigger of decision.triggeredHardTriggers) { + const mandatoryDocs = trigger.rule.mandatoryDocuments + if (mandatoryDocs.includes(documentType)) { + // Gut - wir erstellen ein mandatory document + } else { + // Pruefen ob die mandatory documents des Triggers vorhanden sind + // (nur Hinweis, kein Block) + } + } + + return { + allowed: violations.length === 0, + violations, + adjustments, + checkedRules, + } + } + + /** + * Convenience: Prueft aus einem DraftContext heraus. + */ + checkFromContext( + documentType: ScopeDocumentType, + context: DraftContext + ): ConstraintCheckResult { + // Reconstruct a minimal ScopeDecision from context + const pseudoDecision: ScopeDecision = { + id: 'projected', + determinedLevel: context.decisions.level, + scores: context.decisions.scores, + triggeredHardTriggers: context.decisions.hardTriggers.map(t => ({ + rule: { + id: t.id, + label: t.label, + description: '', + conditionField: '', + conditionOperator: 'EQUALS' as const, + conditionValue: null, + minimumLevel: context.decisions.level, + mandatoryDocuments: [], + dsfaRequired: false, + legalReference: t.legalReference, + }, + matchedValue: null, + explanation: '', + })), + requiredDocuments: context.decisions.requiredDocuments.map(d => ({ + documentType: d.documentType, + label: d.documentType, + required: true, + depth: d.depth, + detailItems: d.detailItems, + estimatedEffort: '', + triggeredBy: [], + })), + riskFlags: context.constraints.riskFlags.map(f => ({ + id: `rf-${f.title}`, + severity: f.severity as 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL', + title: f.title, + description: '', + recommendation: f.recommendation, + })), + gaps: [], + nextActions: [], + reasoning: [], + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + } + + return this.check(documentType, pseudoDecision) + } +} + +/** Singleton-Instanz */ +export const constraintEnforcer = new ConstraintEnforcer() diff --git a/admin-v2/lib/sdk/drafting-engine/intent-classifier.ts b/admin-v2/lib/sdk/drafting-engine/intent-classifier.ts new file mode 100644 index 0000000..41be915 --- /dev/null +++ b/admin-v2/lib/sdk/drafting-engine/intent-classifier.ts @@ -0,0 +1,241 @@ +/** + * Intent Classifier - Leichtgewichtiger Pattern-Matcher + * + * Erkennt den Agent-Modus anhand des Nutzer-Inputs ohne LLM-Call. + * Deutsche und englische Muster werden unterstuetzt. + * + * Confidence-Schwellen: + * - >0.8: Hohe Sicherheit, automatisch anwenden + * - 0.6-0.8: Mittel, Nutzer kann bestaetigen + * - <0.6: Fallback zu 'explain' + */ + +import type { AgentMode, IntentClassification } from './types' +import type { ScopeDocumentType } from '../compliance-scope-types' + +// ============================================================================ +// Pattern Definitions +// ============================================================================ + +interface ModePattern { + mode: AgentMode + patterns: RegExp[] + /** Base-Confidence wenn ein Pattern matched */ + baseConfidence: number +} + +const MODE_PATTERNS: ModePattern[] = [ + { + mode: 'draft', + baseConfidence: 0.85, + patterns: [ + /\b(erstell|generier|entw[iu]rf|entwer[ft]|schreib|verfass|formulier|anlege)/i, + /\b(draft|create|generate|write|compose)\b/i, + /\b(neues?\s+(?:vvt|tom|dsfa|dokument|loeschkonzept|datenschutzerklaerung))\b/i, + /\b(vorlage|template)\s+(erstell|generier)/i, + /\bfuer\s+(?:uns|mich|unser)\b.*\b(erstell|schreib)/i, + ], + }, + { + mode: 'validate', + baseConfidence: 0.80, + patterns: [ + /\b(pruef|validier|check|kontrollier|ueberpruef)\b/i, + /\b(korrekt|richtig|vollstaendig|konsistent|komplett)\b.*\?/i, + /\b(stimmt|passt)\b.*\b(das|mein|unser)\b/i, + /\b(validate|verify|check|review)\b/i, + /\b(fehler|luecken?|maengel)\b.*\b(find|such|zeig)\b/i, + /\bcross[\s-]?check\b/i, + /\b(vvt|tom|dsfa)\b.*\b(konsisten[tz]|widerspruch|uebereinstimm)/i, + ], + }, + { + mode: 'ask', + baseConfidence: 0.75, + patterns: [ + /\bwas\s+fehlt\b/i, + /\b(luecken?|gaps?)\b.*\b(zeig|find|identifizier|analysier)/i, + /\b(unvollstaendig|unfertig|offen)\b/i, + /\bwelche\s+(dokumente?|informationen?|daten)\b.*\b(fehlen?|brauch|benoetig)/i, + /\b(naechste[rn]?\s+schritt|next\s+step|todo)\b/i, + /\bworan\s+(muss|soll)\b/i, + ], + }, +] + +/** Dokumenttyp-Erkennung */ +const DOCUMENT_TYPE_PATTERNS: Array<{ + type: ScopeDocumentType + patterns: RegExp[] +}> = [ + { + type: 'vvt', + patterns: [ + /\bv{1,2}t\b/i, + /\bverarbeitungsverzeichnis\b/i, + /\bverarbeitungstaetigkeit/i, + /\bprocessing\s+activit/i, + /\bart\.?\s*30\b/i, + ], + }, + { + type: 'tom', + patterns: [ + /\btom\b/i, + /\btechnisch.*organisatorisch.*massnahm/i, + /\bart\.?\s*32\b/i, + /\bsicherheitsmassnahm/i, + ], + }, + { + type: 'dsfa', + patterns: [ + /\bdsfa\b/i, + /\bdatenschutz[\s-]?folgenabschaetzung\b/i, + /\bdpia\b/i, + /\bart\.?\s*35\b/i, + /\bimpact\s+assessment\b/i, + ], + }, + { + type: 'dsi', + patterns: [ + /\bdatenschutzerklaerung\b/i, + /\bprivacy\s+policy\b/i, + /\bdsi\b/i, + /\bart\.?\s*13\b/i, + /\bart\.?\s*14\b/i, + ], + }, + { + type: 'lf', + patterns: [ + /\bloeschfrist/i, + /\bloeschkonzept/i, + /\bretention/i, + /\baufbewahr/i, + ], + }, + { + type: 'av_vertrag', + patterns: [ + /\bavv?\b/i, + /\bauftragsverarbeit/i, + /\bdata\s+processing\s+agreement/i, + /\bart\.?\s*28\b/i, + ], + }, + { + type: 'betroffenenrechte', + patterns: [ + /\bbetroffenenrecht/i, + /\bdata\s+subject\s+right/i, + /\bart\.?\s*15\b/i, + /\bauskunft/i, + ], + }, + { + type: 'einwilligung', + patterns: [ + /\beinwillig/i, + /\bconsent/i, + /\bcookie/i, + ], + }, +] + +// ============================================================================ +// Classifier +// ============================================================================ + +export class IntentClassifier { + + /** + * Klassifiziert die Nutzerabsicht anhand des Inputs. + * + * @param input - Die Nutzer-Nachricht + * @returns IntentClassification mit Mode, Confidence, Patterns + */ + classify(input: string): IntentClassification { + const normalized = this.normalize(input) + let bestMatch: IntentClassification = { + mode: 'explain', + confidence: 0.3, + matchedPatterns: [], + } + + for (const modePattern of MODE_PATTERNS) { + const matched: string[] = [] + + for (const pattern of modePattern.patterns) { + if (pattern.test(normalized)) { + matched.push(pattern.source) + } + } + + if (matched.length > 0) { + // Mehr Matches = hoehere Confidence (bis zum Maximum) + const matchBonus = Math.min(matched.length - 1, 2) * 0.05 + const confidence = Math.min(modePattern.baseConfidence + matchBonus, 0.99) + + if (confidence > bestMatch.confidence) { + bestMatch = { + mode: modePattern.mode, + confidence, + matchedPatterns: matched, + } + } + } + } + + // Dokumenttyp erkennen + const detectedDocType = this.detectDocumentType(normalized) + if (detectedDocType) { + bestMatch.detectedDocumentType = detectedDocType + // Dokumenttyp-Erkennung erhoeht Confidence leicht + bestMatch.confidence = Math.min(bestMatch.confidence + 0.05, 0.99) + } + + // Fallback: Bei Confidence <0.6 immer 'explain' + if (bestMatch.confidence < 0.6) { + bestMatch.mode = 'explain' + } + + return bestMatch + } + + /** + * Erkennt den Dokumenttyp aus dem Input. + */ + detectDocumentType(input: string): ScopeDocumentType | undefined { + const normalized = this.normalize(input) + + for (const docPattern of DOCUMENT_TYPE_PATTERNS) { + for (const pattern of docPattern.patterns) { + if (pattern.test(normalized)) { + return docPattern.type + } + } + } + + return undefined + } + + /** + * Normalisiert den Input fuer Pattern-Matching. + * Ersetzt Umlaute, entfernt Sonderzeichen. + */ + private normalize(input: string): string { + return input + .replace(/ä/g, 'ae') + .replace(/ö/g, 'oe') + .replace(/ü/g, 'ue') + .replace(/ß/g, 'ss') + .replace(/Ä/g, 'Ae') + .replace(/Ö/g, 'Oe') + .replace(/Ü/g, 'Ue') + } +} + +/** Singleton-Instanz */ +export const intentClassifier = new IntentClassifier() diff --git a/admin-v2/lib/sdk/drafting-engine/prompts/ask-gap-analysis.ts b/admin-v2/lib/sdk/drafting-engine/prompts/ask-gap-analysis.ts new file mode 100644 index 0000000..6a60543 --- /dev/null +++ b/admin-v2/lib/sdk/drafting-engine/prompts/ask-gap-analysis.ts @@ -0,0 +1,49 @@ +/** + * Gap Analysis Prompt - Lueckenanalyse und gezielte Fragen + */ + +import type { GapContext } from '../types' + +export interface GapAnalysisInput { + context: GapContext + instructions?: string +} + +export function buildGapAnalysisPrompt(input: GapAnalysisInput): string { + const { context, instructions } = input + + return `## Aufgabe: Compliance-Lueckenanalyse + +### Identifizierte Luecken: +${context.gaps.length > 0 + ? context.gaps.map(g => `- [${g.severity}] ${g.title}: ${g.description}`).join('\n') + : '- Keine Luecken identifiziert'} + +### Fehlende Pflichtdokumente: +${context.missingDocuments.length > 0 + ? context.missingDocuments.map(d => `- ${d.label} (Tiefe: ${d.depth}, Aufwand: ${d.estimatedEffort})`).join('\n') + : '- Alle Pflichtdokumente vorhanden'} + +### Unbeantwortete Fragen: +${context.unansweredQuestions.length > 0 + ? context.unansweredQuestions.map(q => `- [${q.blockId}] ${q.question}`).join('\n') + : '- Alle Fragen beantwortet'} + +${instructions ? `### Zusaetzliche Anweisungen: ${instructions}` : ''} + +### Aufgabe: +Analysiere den Stand und stelle EINE gezielte Frage, die die wichtigste Luecke adressiert. +Priorisiere nach: +1. Fehlende Pflichtdokumente +2. Kritische Luecken (HIGH/CRITICAL severity) +3. Unbeantwortete Pflichtfragen +4. Mittlere Luecken + +### Antwort-Format: +Antworte in dieser Struktur: +1. **Statusuebersicht**: Kurze Zusammenfassung des Compliance-Stands (2-3 Saetze) +2. **Wichtigste Luecke**: Was fehlt am dringendsten? +3. **Gezielte Frage**: Eine konkrete Frage an den Nutzer +4. **Warum wichtig**: Warum muss diese Luecke geschlossen werden? +5. **Empfohlener naechster Schritt**: Link/Verweis zum SDK-Modul` +} diff --git a/admin-v2/lib/sdk/drafting-engine/prompts/draft-dsfa.ts b/admin-v2/lib/sdk/drafting-engine/prompts/draft-dsfa.ts new file mode 100644 index 0000000..7e1e898 --- /dev/null +++ b/admin-v2/lib/sdk/drafting-engine/prompts/draft-dsfa.ts @@ -0,0 +1,91 @@ +/** + * DSFA Draft Prompt - Datenschutz-Folgenabschaetzung (Art. 35 DSGVO) + */ + +import type { DraftContext } from '../types' + +export interface DSFADraftInput { + context: DraftContext + processingDescription?: string + instructions?: string +} + +export function buildDSFADraftPrompt(input: DSFADraftInput): string { + const { context, processingDescription, instructions } = input + const level = context.decisions.level + const depthItems = context.constraints.depthRequirements.detailItems + const hardTriggers = context.decisions.hardTriggers + + return `## Aufgabe: DSFA entwerfen (Art. 35 DSGVO) + +### Unternehmensprofil +- Name: ${context.companyProfile.name} +- Branche: ${context.companyProfile.industry} +- Mitarbeiter: ${context.companyProfile.employeeCount} + +### Compliance-Level: ${level} +Tiefe: ${context.constraints.depthRequirements.depth} + +### Hard Triggers (Gruende fuer DSFA-Pflicht): +${hardTriggers.length > 0 + ? hardTriggers.map(t => `- ${t.id}: ${t.label} (${t.legalReference})`).join('\n') + : '- Keine Hard Triggers (DSFA auf Wunsch)'} + +### Erforderliche Inhalte: +${depthItems.map((item, i) => `${i + 1}. ${item}`).join('\n')} + +${processingDescription ? `### Beschreibung der Verarbeitung: ${processingDescription}` : ''} +${instructions ? `### Zusaetzliche Anweisungen: ${instructions}` : ''} + +### Antwort-Format +Antworte als JSON: +{ + "sections": [ + { + "id": "beschreibung", + "title": "Systematische Beschreibung der Verarbeitung", + "content": "...", + "schemaField": "processingDescription" + }, + { + "id": "notwendigkeit", + "title": "Notwendigkeit und Verhaeltnismaessigkeit", + "content": "...", + "schemaField": "necessityAssessment" + }, + { + "id": "risikobewertung", + "title": "Bewertung der Risiken fuer die Rechte und Freiheiten", + "content": "...", + "schemaField": "riskAssessment" + }, + { + "id": "massnahmen", + "title": "Massnahmen zur Eindaemmung der Risiken", + "content": "...", + "schemaField": "mitigationMeasures" + }, + { + "id": "stellungnahme_dsb", + "title": "Stellungnahme des Datenschutzbeauftragten", + "content": "...", + "schemaField": "dpoOpinion" + }, + { + "id": "standpunkt_betroffene", + "title": "Standpunkt der betroffenen Personen", + "content": "...", + "schemaField": "dataSubjectView" + }, + { + "id": "ergebnis", + "title": "Ergebnis und Empfehlung", + "content": "...", + "schemaField": "conclusion" + } + ] +} + +Halte die Tiefe exakt auf Level ${level}. +Nutze WP248-Kriterien als Leitfaden fuer die Risikobewertung.` +} diff --git a/admin-v2/lib/sdk/drafting-engine/prompts/draft-loeschfristen.ts b/admin-v2/lib/sdk/drafting-engine/prompts/draft-loeschfristen.ts new file mode 100644 index 0000000..ddf5d8e --- /dev/null +++ b/admin-v2/lib/sdk/drafting-engine/prompts/draft-loeschfristen.ts @@ -0,0 +1,78 @@ +/** + * Loeschfristen Draft Prompt - Loeschkonzept + */ + +import type { DraftContext } from '../types' + +export interface LoeschfristenDraftInput { + context: DraftContext + instructions?: string +} + +export function buildLoeschfristenDraftPrompt(input: LoeschfristenDraftInput): string { + const { context, instructions } = input + const level = context.decisions.level + const depthItems = context.constraints.depthRequirements.detailItems + + return `## Aufgabe: Loeschkonzept / Loeschfristen entwerfen + +### Unternehmensprofil +- Name: ${context.companyProfile.name} +- Branche: ${context.companyProfile.industry} +- Mitarbeiter: ${context.companyProfile.employeeCount} + +### Compliance-Level: ${level} +Tiefe: ${context.constraints.depthRequirements.depth} + +### Erforderliche Inhalte: +${depthItems.map((item, i) => `${i + 1}. ${item}`).join('\n')} + +${context.existingDocumentData ? `### Bestehende Loeschfristen: ${JSON.stringify(context.existingDocumentData).slice(0, 500)}` : ''} +${instructions ? `### Zusaetzliche Anweisungen: ${instructions}` : ''} + +### Antwort-Format +Antworte als JSON: +{ + "sections": [ + { + "id": "grundsaetze", + "title": "Grundsaetze der Datenlöschung", + "content": "...", + "schemaField": "principles" + }, + { + "id": "kategorien", + "title": "Datenkategorien und Loeschfristen", + "content": "Tabellarische Uebersicht...", + "schemaField": "retentionSchedule" + }, + { + "id": "gesetzliche_fristen", + "title": "Gesetzliche Aufbewahrungsfristen", + "content": "HGB, AO, weitere...", + "schemaField": "legalRetention" + }, + { + "id": "loeschprozess", + "title": "Technischer Loeschprozess", + "content": "...", + "schemaField": "deletionProcess" + }, + { + "id": "verantwortlichkeiten", + "title": "Verantwortlichkeiten", + "content": "...", + "schemaField": "responsibilities" + }, + { + "id": "ausnahmen", + "title": "Ausnahmen und Sonderfaelle", + "content": "...", + "schemaField": "exceptions" + } + ] +} + +Halte die Tiefe exakt auf Level ${level}. +Beruecksichtige branchenspezifische Aufbewahrungsfristen fuer ${context.companyProfile.industry}.` +} diff --git a/admin-v2/lib/sdk/drafting-engine/prompts/draft-privacy-policy.ts b/admin-v2/lib/sdk/drafting-engine/prompts/draft-privacy-policy.ts new file mode 100644 index 0000000..a417d42 --- /dev/null +++ b/admin-v2/lib/sdk/drafting-engine/prompts/draft-privacy-policy.ts @@ -0,0 +1,102 @@ +/** + * Privacy Policy Draft Prompt - Datenschutzerklaerung (Art. 13/14 DSGVO) + */ + +import type { DraftContext } from '../types' + +export interface PrivacyPolicyDraftInput { + context: DraftContext + websiteUrl?: string + instructions?: string +} + +export function buildPrivacyPolicyDraftPrompt(input: PrivacyPolicyDraftInput): string { + const { context, websiteUrl, instructions } = input + const level = context.decisions.level + const depthItems = context.constraints.depthRequirements.detailItems + + return `## Aufgabe: Datenschutzerklaerung entwerfen (Art. 13/14 DSGVO) + +### Unternehmensprofil +- Name: ${context.companyProfile.name} +- Branche: ${context.companyProfile.industry} +${context.companyProfile.dataProtectionOfficer ? `- DSB: ${context.companyProfile.dataProtectionOfficer.name} (${context.companyProfile.dataProtectionOfficer.email})` : ''} +${websiteUrl ? `- Website: ${websiteUrl}` : ''} + +### Compliance-Level: ${level} +Tiefe: ${context.constraints.depthRequirements.depth} + +### Erforderliche Inhalte: +${depthItems.map((item, i) => `${i + 1}. ${item}`).join('\n')} + +${instructions ? `### Zusaetzliche Anweisungen: ${instructions}` : ''} + +### Antwort-Format +Antworte als JSON: +{ + "sections": [ + { + "id": "verantwortlicher", + "title": "Verantwortlicher", + "content": "...", + "schemaField": "controller" + }, + { + "id": "dsb", + "title": "Datenschutzbeauftragter", + "content": "...", + "schemaField": "dpo" + }, + { + "id": "verarbeitungen", + "title": "Verarbeitungstaetigkeiten und Zwecke", + "content": "...", + "schemaField": "processingPurposes" + }, + { + "id": "rechtsgrundlagen", + "title": "Rechtsgrundlagen der Verarbeitung", + "content": "...", + "schemaField": "legalBases" + }, + { + "id": "empfaenger", + "title": "Empfaenger und Datenweitergabe", + "content": "...", + "schemaField": "recipients" + }, + { + "id": "drittland", + "title": "Uebermittlung in Drittlaender", + "content": "...", + "schemaField": "thirdCountryTransfers" + }, + { + "id": "speicherdauer", + "title": "Speicherdauer", + "content": "...", + "schemaField": "retentionPeriods" + }, + { + "id": "betroffenenrechte", + "title": "Ihre Rechte als betroffene Person", + "content": "...", + "schemaField": "dataSubjectRights" + }, + { + "id": "cookies", + "title": "Cookies und Tracking", + "content": "...", + "schemaField": "cookies" + }, + { + "id": "aenderungen", + "title": "Aenderungen dieser Datenschutzerklaerung", + "content": "...", + "schemaField": "changes" + } + ] +} + +Halte die Tiefe exakt auf Level ${level}.` +} diff --git a/admin-v2/lib/sdk/drafting-engine/prompts/draft-tom.ts b/admin-v2/lib/sdk/drafting-engine/prompts/draft-tom.ts new file mode 100644 index 0000000..5f97d52 --- /dev/null +++ b/admin-v2/lib/sdk/drafting-engine/prompts/draft-tom.ts @@ -0,0 +1,99 @@ +/** + * TOM Draft Prompt - Technische und Organisatorische Massnahmen (Art. 32 DSGVO) + */ + +import type { DraftContext } from '../types' + +export interface TOMDraftInput { + context: DraftContext + focusArea?: string + instructions?: string +} + +export function buildTOMDraftPrompt(input: TOMDraftInput): string { + const { context, focusArea, instructions } = input + const level = context.decisions.level + const depthItems = context.constraints.depthRequirements.detailItems + + return `## Aufgabe: TOM-Dokument entwerfen (Art. 32 DSGVO) + +### Unternehmensprofil +- Name: ${context.companyProfile.name} +- Branche: ${context.companyProfile.industry} +- Mitarbeiter: ${context.companyProfile.employeeCount} + +### Compliance-Level: ${level} +Tiefe: ${context.constraints.depthRequirements.depth} + +### Erforderliche Inhalte fuer Level ${level}: +${depthItems.map((item, i) => `${i + 1}. ${item}`).join('\n')} + +### Constraints +${context.constraints.boundaries.map(b => `- ${b}`).join('\n')} + +${context.constraints.riskFlags.length > 0 ? `### Risiko-Flags +${context.constraints.riskFlags.map(f => `- [${f.severity}] ${f.title}`).join('\n')}` : ''} + +${focusArea ? `### Fokusbereich: ${focusArea}` : ''} +${instructions ? `### Zusaetzliche Anweisungen: ${instructions}` : ''} + +${context.existingDocumentData ? `### Bestehende TOM: ${JSON.stringify(context.existingDocumentData).slice(0, 500)}` : ''} + +### Antwort-Format +Antworte als JSON: +{ + "sections": [ + { + "id": "zutrittskontrolle", + "title": "Zutrittskontrolle", + "content": "Massnahmen die unbefugten Zutritt zu Datenverarbeitungsanlagen verhindern...", + "schemaField": "accessControl" + }, + { + "id": "zugangskontrolle", + "title": "Zugangskontrolle", + "content": "Massnahmen gegen unbefugte Systemnutzung...", + "schemaField": "systemAccessControl" + }, + { + "id": "zugriffskontrolle", + "title": "Zugriffskontrolle", + "content": "Massnahmen zur Sicherstellung berechtigter Datenzugriffe...", + "schemaField": "dataAccessControl" + }, + { + "id": "weitergabekontrolle", + "title": "Weitergabekontrolle / Uebertragungssicherheit", + "content": "Massnahmen bei Datenuebertragung und -transport...", + "schemaField": "transferControl" + }, + { + "id": "eingabekontrolle", + "title": "Eingabekontrolle", + "content": "Nachvollziehbarkeit von Dateneingaben...", + "schemaField": "inputControl" + }, + { + "id": "auftragskontrolle", + "title": "Auftragskontrolle", + "content": "Massnahmen zur weisungsgemaessen Auftragsverarbeitung...", + "schemaField": "orderControl" + }, + { + "id": "verfuegbarkeitskontrolle", + "title": "Verfuegbarkeitskontrolle", + "content": "Schutz gegen Datenverlust...", + "schemaField": "availabilityControl" + }, + { + "id": "trennungsgebot", + "title": "Trennungsgebot", + "content": "Getrennte Verarbeitung fuer verschiedene Zwecke...", + "schemaField": "separationControl" + } + ] +} + +Fuelle fehlende Informationen mit [PLATZHALTER: ...]. +Halte die Tiefe exakt auf Level ${level}.` +} diff --git a/admin-v2/lib/sdk/drafting-engine/prompts/draft-vvt.ts b/admin-v2/lib/sdk/drafting-engine/prompts/draft-vvt.ts new file mode 100644 index 0000000..bf1aa75 --- /dev/null +++ b/admin-v2/lib/sdk/drafting-engine/prompts/draft-vvt.ts @@ -0,0 +1,109 @@ +/** + * VVT Draft Prompt - Verarbeitungsverzeichnis (Art. 30 DSGVO) + */ + +import type { DraftContext } from '../types' + +export interface VVTDraftInput { + context: DraftContext + activityName?: string + activityPurpose?: string + instructions?: string +} + +export function buildVVTDraftPrompt(input: VVTDraftInput): string { + const { context, activityName, activityPurpose, instructions } = input + const level = context.decisions.level + const depthItems = context.constraints.depthRequirements.detailItems + + return `## Aufgabe: VVT-Eintrag entwerfen (Art. 30 DSGVO) + +### Unternehmensprofil +- Name: ${context.companyProfile.name} +- Branche: ${context.companyProfile.industry} +- Mitarbeiter: ${context.companyProfile.employeeCount} +- Geschaeftsmodell: ${context.companyProfile.businessModel} +${context.companyProfile.dataProtectionOfficer ? `- DSB: ${context.companyProfile.dataProtectionOfficer.name} (${context.companyProfile.dataProtectionOfficer.email})` : '- DSB: Nicht benannt'} + +### Compliance-Level: ${level} +Tiefe: ${context.constraints.depthRequirements.depth} + +### Erforderliche Inhalte fuer Level ${level}: +${depthItems.map((item, i) => `${i + 1}. ${item}`).join('\n')} + +### Constraints +${context.constraints.boundaries.map(b => `- ${b}`).join('\n')} + +${context.constraints.riskFlags.length > 0 ? `### Risiko-Flags +${context.constraints.riskFlags.map(f => `- [${f.severity}] ${f.title}: ${f.recommendation}`).join('\n')}` : ''} + +${activityName ? `### Gewuenschte Verarbeitungstaetigkeit: ${activityName}` : ''} +${activityPurpose ? `### Zweck: ${activityPurpose}` : ''} +${instructions ? `### Zusaetzliche Anweisungen: ${instructions}` : ''} + +${context.existingDocumentData ? `### Bestehende VVT-Eintraege: ${JSON.stringify(context.existingDocumentData).slice(0, 500)}` : ''} + +### Antwort-Format +Antworte als JSON: +{ + "sections": [ + { + "id": "bezeichnung", + "title": "Bezeichnung der Verarbeitungstaetigkeit", + "content": "...", + "schemaField": "name" + }, + { + "id": "verantwortlicher", + "title": "Verantwortlicher", + "content": "...", + "schemaField": "controller" + }, + { + "id": "zweck", + "title": "Zweck der Verarbeitung", + "content": "...", + "schemaField": "purpose" + }, + { + "id": "rechtsgrundlage", + "title": "Rechtsgrundlage", + "content": "...", + "schemaField": "legalBasis" + }, + { + "id": "betroffene", + "title": "Kategorien betroffener Personen", + "content": "...", + "schemaField": "dataSubjects" + }, + { + "id": "datenkategorien", + "title": "Kategorien personenbezogener Daten", + "content": "...", + "schemaField": "dataCategories" + }, + { + "id": "empfaenger", + "title": "Empfaenger", + "content": "...", + "schemaField": "recipients" + }, + { + "id": "speicherdauer", + "title": "Speicherdauer / Loeschfristen", + "content": "...", + "schemaField": "retentionPeriod" + }, + { + "id": "tom_referenz", + "title": "TOM-Referenz", + "content": "...", + "schemaField": "tomReference" + } + ] +} + +Fuelle fehlende Informationen mit [PLATZHALTER: Beschreibung was hier eingetragen werden muss]. +Halte die Tiefe exakt auf Level ${level} (${context.constraints.depthRequirements.depth}).` +} diff --git a/admin-v2/lib/sdk/drafting-engine/prompts/index.ts b/admin-v2/lib/sdk/drafting-engine/prompts/index.ts new file mode 100644 index 0000000..dde2726 --- /dev/null +++ b/admin-v2/lib/sdk/drafting-engine/prompts/index.ts @@ -0,0 +1,11 @@ +/** + * Drafting Engine Prompts - Re-Exports + */ + +export { buildVVTDraftPrompt, type VVTDraftInput } from './draft-vvt' +export { buildTOMDraftPrompt, type TOMDraftInput } from './draft-tom' +export { buildDSFADraftPrompt, type DSFADraftInput } from './draft-dsfa' +export { buildPrivacyPolicyDraftPrompt, type PrivacyPolicyDraftInput } from './draft-privacy-policy' +export { buildLoeschfristenDraftPrompt, type LoeschfristenDraftInput } from './draft-loeschfristen' +export { buildCrossCheckPrompt, type CrossCheckInput } from './validate-cross-check' +export { buildGapAnalysisPrompt, type GapAnalysisInput } from './ask-gap-analysis' diff --git a/admin-v2/lib/sdk/drafting-engine/prompts/validate-cross-check.ts b/admin-v2/lib/sdk/drafting-engine/prompts/validate-cross-check.ts new file mode 100644 index 0000000..825ae60 --- /dev/null +++ b/admin-v2/lib/sdk/drafting-engine/prompts/validate-cross-check.ts @@ -0,0 +1,66 @@ +/** + * Cross-Document Validation Prompt + */ + +import type { ValidationContext } from '../types' + +export interface CrossCheckInput { + context: ValidationContext + focusDocuments?: string[] + instructions?: string +} + +export function buildCrossCheckPrompt(input: CrossCheckInput): string { + const { context, focusDocuments, instructions } = input + + return `## Aufgabe: Cross-Dokument-Konsistenzpruefung + +### Scope-Level: ${context.scopeLevel} + +### Vorhandene Dokumente: +${context.documents.map(d => `- ${d.type}: ${d.contentSummary}`).join('\n')} + +### Cross-Referenzen: +- VVT-Kategorien: ${context.crossReferences.vvtCategories.join(', ') || 'Keine'} +- DSFA-Risiken: ${context.crossReferences.dsfaRisks.join(', ') || 'Keine'} +- TOM-Controls: ${context.crossReferences.tomControls.join(', ') || 'Keine'} +- Loeschfristen-Kategorien: ${context.crossReferences.retentionCategories.join(', ') || 'Keine'} + +### Tiefenpruefung pro Dokument: +${context.documents.map(d => { + const req = context.depthRequirements[d.type] + return req ? `- ${d.type}: Erforderlich=${req.required}, Tiefe=${req.depth}` : `- ${d.type}: Keine Requirements` + }).join('\n')} + +${focusDocuments ? `### Fokus auf: ${focusDocuments.join(', ')}` : ''} +${instructions ? `### Zusaetzliche Anweisungen: ${instructions}` : ''} + +### Pruefkriterien: +1. Jede VVT-Taetigkeit muss einen TOM-Verweis haben +2. Jede VVT-Kategorie muss eine Loeschfrist haben +3. Bei DSFA-pflichtigen Verarbeitungen muss eine DSFA existieren +4. TOM-Massnahmen muessen zum Risikoprofil passen +5. Loeschfristen duerfen gesetzliche Minima nicht unterschreiten +6. Dokument-Tiefe muss Level ${context.scopeLevel} entsprechen + +### Antwort-Format +Antworte als JSON: +{ + "passed": true/false, + "errors": [ + { + "id": "ERR-001", + "severity": "error", + "category": "scope_violation|inconsistency|missing_content|depth_mismatch|cross_reference", + "title": "...", + "description": "...", + "documentType": "vvt|tom|dsfa|...", + "crossReferenceType": "...", + "legalReference": "Art. ... DSGVO", + "suggestion": "..." + } + ], + "warnings": [...], + "suggestions": [...] +}` +} diff --git a/admin-v2/lib/sdk/drafting-engine/state-projector.ts b/admin-v2/lib/sdk/drafting-engine/state-projector.ts new file mode 100644 index 0000000..67e5866 --- /dev/null +++ b/admin-v2/lib/sdk/drafting-engine/state-projector.ts @@ -0,0 +1,337 @@ +/** + * State Projector - Token-budgetierte Projektion des SDK-State + * + * Extrahiert aus dem vollen SDKState (der ~50k Tokens betragen kann) nur die + * relevanten Slices fuer den jeweiligen Agent-Modus. + * + * Token-Budgets: + * - Draft: ~1500 Tokens + * - Ask: ~600 Tokens + * - Validate: ~2000 Tokens + */ + +import type { SDKState, CompanyProfile } from '../types' +import type { + ComplianceScopeState, + ScopeDecision, + ScopeDocumentType, + ScopeGap, + RequiredDocument, + RiskFlag, + DOCUMENT_SCOPE_MATRIX, + DocumentDepthRequirement, +} from '../compliance-scope-types' +import { DOCUMENT_SCOPE_MATRIX as DOC_MATRIX, DOCUMENT_TYPE_LABELS } from '../compliance-scope-types' +import type { + DraftContext, + GapContext, + ValidationContext, +} from './types' + +// ============================================================================ +// State Projector +// ============================================================================ + +export class StateProjector { + + /** + * Projiziert den SDKState fuer Draft-Operationen. + * Fokus: Scope-Decision, Company-Profile, Dokument-spezifische Constraints. + * + * ~1500 Tokens + */ + projectForDraft( + state: SDKState, + documentType: ScopeDocumentType + ): DraftContext { + const decision = state.complianceScope?.decision ?? null + const level = decision?.determinedLevel ?? 'L1' + const depthReq = DOC_MATRIX[documentType]?.[level] ?? { + required: false, + depth: 'Basis', + detailItems: [], + estimatedEffort: 'N/A', + } + + return { + decisions: { + level, + scores: decision?.scores ?? { + risk_score: 0, + complexity_score: 0, + assurance_need: 0, + composite_score: 0, + }, + hardTriggers: (decision?.triggeredHardTriggers ?? []).map(t => ({ + id: t.rule.id, + label: t.rule.label, + legalReference: t.rule.legalReference, + })), + requiredDocuments: (decision?.requiredDocuments ?? []) + .filter(d => d.required) + .map(d => ({ + documentType: d.documentType, + depth: d.depth, + detailItems: d.detailItems, + })), + }, + companyProfile: this.projectCompanyProfile(state.companyProfile), + constraints: { + depthRequirements: depthReq, + riskFlags: (decision?.riskFlags ?? []).map(f => ({ + severity: f.severity, + title: f.title, + recommendation: f.recommendation, + })), + boundaries: this.deriveBoundaries(decision, documentType), + }, + existingDocumentData: this.extractExistingDocumentData(state, documentType), + } + } + + /** + * Projiziert den SDKState fuer Ask-Operationen. + * Fokus: Luecken, unbeantwortete Fragen, fehlende Dokumente. + * + * ~600 Tokens + */ + projectForAsk(state: SDKState): GapContext { + const decision = state.complianceScope?.decision ?? null + + // Fehlende Pflichtdokumente ermitteln + const requiredDocs = (decision?.requiredDocuments ?? []).filter(d => d.required) + const existingDocTypes = this.getExistingDocumentTypes(state) + const missingDocuments = requiredDocs + .filter(d => !existingDocTypes.includes(d.documentType)) + .map(d => ({ + documentType: d.documentType, + label: DOCUMENT_TYPE_LABELS[d.documentType] ?? d.documentType, + depth: d.depth, + estimatedEffort: d.estimatedEffort, + })) + + // Gaps aus der Scope-Decision + const gaps = (decision?.gaps ?? []).map(g => ({ + id: g.id, + severity: g.severity, + title: g.title, + description: g.description, + relatedDocuments: g.relatedDocuments, + })) + + // Unbeantwortete Fragen (aus dem Scope-Profiling) + const answers = state.complianceScope?.answers ?? [] + const answeredIds = new Set(answers.map(a => a.questionId)) + + return { + unansweredQuestions: [], // Populated dynamically from question catalog + gaps, + missingDocuments, + } + } + + /** + * Projiziert den SDKState fuer Validate-Operationen. + * Fokus: Cross-Dokument-Konsistenz, Scope-Compliance. + * + * ~2000 Tokens + */ + projectForValidate( + state: SDKState, + documentTypes: ScopeDocumentType[] + ): ValidationContext { + const decision = state.complianceScope?.decision ?? null + const level = decision?.determinedLevel ?? 'L1' + + // Dokument-Zusammenfassungen sammeln + const documents = documentTypes.map(type => ({ + type, + contentSummary: this.summarizeDocument(state, type), + structuredData: this.extractExistingDocumentData(state, type), + })) + + // Cross-Referenzen extrahieren + const crossReferences = { + vvtCategories: (state.vvt ?? []).map(v => + typeof v === 'object' && v !== null && 'name' in v ? String((v as Record).name) : '' + ).filter(Boolean), + dsfaRisks: state.dsfa + ? ['DSFA vorhanden'] + : [], + tomControls: (state.toms ?? []).map(t => + typeof t === 'object' && t !== null && 'name' in t ? String((t as Record).name) : '' + ).filter(Boolean), + retentionCategories: (state.retentionPolicies ?? []).map(p => + typeof p === 'object' && p !== null && 'name' in p ? String((p as Record).name) : '' + ).filter(Boolean), + } + + // Depth-Requirements fuer alle angefragten Typen + const depthRequirements: Record = {} + for (const type of documentTypes) { + depthRequirements[type] = DOC_MATRIX[type]?.[level] ?? { + required: false, + depth: 'Basis', + detailItems: [], + estimatedEffort: 'N/A', + } + } + + return { + documents, + crossReferences, + scopeLevel: level, + depthRequirements: depthRequirements as Record, + } + } + + // ========================================================================== + // Private Helpers + // ========================================================================== + + private projectCompanyProfile( + profile: CompanyProfile | null + ): DraftContext['companyProfile'] { + if (!profile) { + return { + name: 'Unbekannt', + industry: 'Unbekannt', + employeeCount: 0, + businessModel: 'Unbekannt', + isPublicSector: false, + } + } + + return { + name: profile.companyName ?? profile.name ?? 'Unbekannt', + industry: profile.industry ?? 'Unbekannt', + employeeCount: typeof profile.employeeCount === 'number' + ? profile.employeeCount + : parseInt(String(profile.employeeCount ?? '0'), 10) || 0, + businessModel: profile.businessModel ?? 'Unbekannt', + isPublicSector: profile.isPublicSector ?? false, + ...(profile.dataProtectionOfficer ? { + dataProtectionOfficer: { + name: profile.dataProtectionOfficer.name ?? '', + email: profile.dataProtectionOfficer.email ?? '', + }, + } : {}), + } + } + + /** + * Leitet Grenzen (Boundaries) ab, die der Agent nicht ueberschreiten darf. + */ + private deriveBoundaries( + decision: ScopeDecision | null, + documentType: ScopeDocumentType + ): string[] { + const boundaries: string[] = [] + const level = decision?.determinedLevel ?? 'L1' + + // Grundregel: Scope-Engine ist autoritativ + boundaries.push( + `Maximale Dokumenttiefe: ${level} (${DOC_MATRIX[documentType]?.[level]?.depth ?? 'Basis'})` + ) + + // DSFA-Boundary + if (documentType === 'dsfa') { + const dsfaRequired = decision?.triggeredHardTriggers?.some( + t => t.rule.dsfaRequired + ) ?? false + if (!dsfaRequired && level !== 'L4') { + boundaries.push('DSFA ist laut Scope-Engine NICHT erforderlich. Nur auf expliziten Wunsch erstellen.') + } + } + + // Dokument nicht in requiredDocuments? + const isRequired = decision?.requiredDocuments?.some( + d => d.documentType === documentType && d.required + ) ?? false + if (!isRequired) { + boundaries.push( + `Dokument "${DOCUMENT_TYPE_LABELS[documentType] ?? documentType}" ist auf Level ${level} nicht als Pflicht eingestuft.` + ) + } + + return boundaries + } + + /** + * Extrahiert bereits vorhandene Dokumentdaten aus dem SDK-State. + */ + private extractExistingDocumentData( + state: SDKState, + documentType: ScopeDocumentType + ): Record | undefined { + switch (documentType) { + case 'vvt': + return state.vvt?.length ? { entries: state.vvt.slice(0, 5), totalCount: state.vvt.length } : undefined + case 'tom': + return state.toms?.length ? { entries: state.toms.slice(0, 5), totalCount: state.toms.length } : undefined + case 'lf': + return state.retentionPolicies?.length + ? { entries: state.retentionPolicies.slice(0, 5), totalCount: state.retentionPolicies.length } + : undefined + case 'dsfa': + return state.dsfa ? { assessment: state.dsfa } : undefined + case 'dsi': + return state.documents?.length + ? { entries: state.documents.slice(0, 3), totalCount: state.documents.length } + : undefined + case 'einwilligung': + return state.consents?.length + ? { entries: state.consents.slice(0, 5), totalCount: state.consents.length } + : undefined + default: + return undefined + } + } + + /** + * Ermittelt welche Dokumenttypen bereits im State vorhanden sind. + */ + private getExistingDocumentTypes(state: SDKState): ScopeDocumentType[] { + const types: ScopeDocumentType[] = [] + if (state.vvt?.length) types.push('vvt') + if (state.toms?.length) types.push('tom') + if (state.retentionPolicies?.length) types.push('lf') + if (state.dsfa) types.push('dsfa') + if (state.documents?.length) types.push('dsi') + if (state.consents?.length) types.push('einwilligung') + if (state.cookieBanner) types.push('einwilligung') + return types + } + + /** + * Erstellt eine kurze Zusammenfassung eines Dokuments fuer Validierung. + */ + private summarizeDocument( + state: SDKState, + documentType: ScopeDocumentType + ): string { + switch (documentType) { + case 'vvt': + return state.vvt?.length + ? `${state.vvt.length} Verarbeitungstaetigkeiten erfasst` + : 'Keine VVT-Eintraege vorhanden' + case 'tom': + return state.toms?.length + ? `${state.toms.length} TOM-Massnahmen definiert` + : 'Keine TOM-Massnahmen vorhanden' + case 'lf': + return state.retentionPolicies?.length + ? `${state.retentionPolicies.length} Loeschfristen definiert` + : 'Keine Loeschfristen vorhanden' + case 'dsfa': + return state.dsfa + ? 'DSFA vorhanden' + : 'Keine DSFA vorhanden' + default: + return `Dokument ${DOCUMENT_TYPE_LABELS[documentType] ?? documentType}` + } + } +} + +/** Singleton-Instanz */ +export const stateProjector = new StateProjector() diff --git a/admin-v2/lib/sdk/drafting-engine/types.ts b/admin-v2/lib/sdk/drafting-engine/types.ts new file mode 100644 index 0000000..80d7276 --- /dev/null +++ b/admin-v2/lib/sdk/drafting-engine/types.ts @@ -0,0 +1,279 @@ +/** + * Drafting Engine - Type Definitions + * + * Typen fuer die 4 Agent-Rollen: Explain, Ask, Draft, Validate + * Die Drafting Engine erweitert den Compliance Advisor um aktive Dokumententwurfs- + * und Validierungsfaehigkeiten, stets unter Beachtung der deterministischen Scope-Engine. + */ + +import type { + ComplianceDepthLevel, + ComplianceScores, + ScopeDecision, + ScopeDocumentType, + ScopeGap, + RequiredDocument, + RiskFlag, + DocumentDepthRequirement, + ScopeProfilingQuestion, +} from '../compliance-scope-types' +import type { CompanyProfile } from '../types' + +// ============================================================================ +// Agent Mode +// ============================================================================ + +/** Die 4 Agent-Rollen */ +export type AgentMode = 'explain' | 'ask' | 'draft' | 'validate' + +/** Confidence-Score fuer Intent-Erkennung */ +export interface IntentClassification { + mode: AgentMode + confidence: number + matchedPatterns: string[] + /** Falls Draft oder Validate: erkannter Dokumenttyp */ + detectedDocumentType?: ScopeDocumentType +} + +// ============================================================================ +// Draft Context (fuer Draft-Mode) +// ============================================================================ + +/** Projizierter State fuer Draft-Operationen (~1500 Tokens) */ +export interface DraftContext { + /** Scope-Entscheidung (Level, Scores, Hard Triggers) */ + decisions: { + level: ComplianceDepthLevel + scores: ComplianceScores + hardTriggers: Array<{ id: string; label: string; legalReference: string }> + requiredDocuments: Array<{ + documentType: ScopeDocumentType + depth: string + detailItems: string[] + }> + } + /** Firmenprofil-Auszug */ + companyProfile: { + name: string + industry: string + employeeCount: number + businessModel: string + isPublicSector: boolean + dataProtectionOfficer?: { name: string; email: string } + } + /** Constraints aus der Scope-Engine */ + constraints: { + depthRequirements: DocumentDepthRequirement + riskFlags: Array<{ severity: string; title: string; recommendation: string }> + boundaries: string[] + } + /** Optional: bestehende Dokumentdaten aus dem SDK-State */ + existingDocumentData?: Record +} + +// ============================================================================ +// Gap Context (fuer Ask-Mode) +// ============================================================================ + +/** Projizierter State fuer Ask-Operationen (~600 Tokens) */ +export interface GapContext { + /** Noch unbeantwortete Fragen aus dem Scope-Profiling */ + unansweredQuestions: Array<{ + id: string + question: string + type: string + blockId: string + }> + /** Identifizierte Luecken */ + gaps: Array<{ + id: string + severity: string + title: string + description: string + relatedDocuments: ScopeDocumentType[] + }> + /** Fehlende Pflichtdokumente */ + missingDocuments: Array<{ + documentType: ScopeDocumentType + label: string + depth: string + estimatedEffort: string + }> +} + +// ============================================================================ +// Validation Context (fuer Validate-Mode) +// ============================================================================ + +/** Projizierter State fuer Validate-Operationen (~2000 Tokens) */ +export interface ValidationContext { + /** Zu validierende Dokumente */ + documents: Array<{ + type: ScopeDocumentType + /** Zusammenfassung/Auszug des Inhalts */ + contentSummary: string + /** Strukturierte Daten falls vorhanden */ + structuredData?: Record + }> + /** Cross-Referenzen zwischen Dokumenten */ + crossReferences: { + /** VVT Kategorien (Verarbeitungstaetigkeiten) */ + vvtCategories: string[] + /** DSFA Risiken */ + dsfaRisks: string[] + /** TOM Controls */ + tomControls: string[] + /** Loeschfristen-Kategorien */ + retentionCategories: string[] + } + /** Scope-Level fuer Tiefenpruefung */ + scopeLevel: ComplianceDepthLevel + /** Relevante Depth-Requirements */ + depthRequirements: Record +} + +// ============================================================================ +// Validation Result +// ============================================================================ + +export type ValidationSeverity = 'error' | 'warning' | 'suggestion' + +export interface ValidationFinding { + id: string + severity: ValidationSeverity + category: 'scope_violation' | 'inconsistency' | 'missing_content' | 'depth_mismatch' | 'cross_reference' + title: string + description: string + /** Betroffenes Dokument */ + documentType: ScopeDocumentType + /** Optional: Referenz zu anderem Dokument */ + crossReferenceType?: ScopeDocumentType + /** Rechtsgrundlage falls relevant */ + legalReference?: string + /** Vorschlag zur Behebung */ + suggestion?: string + /** Kann automatisch uebernommen werden */ + autoFixable?: boolean +} + +export interface ValidationResult { + passed: boolean + timestamp: string + scopeLevel: ComplianceDepthLevel + errors: ValidationFinding[] + warnings: ValidationFinding[] + suggestions: ValidationFinding[] +} + +// ============================================================================ +// Draft Session +// ============================================================================ + +export interface DraftRevision { + id: string + content: string + sections: DraftSection[] + createdAt: string + instruction?: string +} + +export interface DraftSection { + id: string + title: string + content: string + /** Mapping zum Dokumentschema (z.B. VVT-Feld) */ + schemaField?: string +} + +export interface DraftSession { + id: string + mode: AgentMode + documentType: ScopeDocumentType + /** Aktueller Draft-Inhalt */ + currentDraft: DraftRevision | null + /** Alle bisherigen Revisionen */ + revisions: DraftRevision[] + /** Validierungszustand */ + validationState: ValidationResult | null + /** Constraint-Check Ergebnis */ + constraintCheck: ConstraintCheckResult | null + createdAt: string + updatedAt: string +} + +// ============================================================================ +// Constraint Check (Hard Gate) +// ============================================================================ + +export interface ConstraintCheckResult { + /** Darf der Draft erstellt werden? */ + allowed: boolean + /** Verletzungen die den Draft blockieren */ + violations: string[] + /** Anpassungen die vorgenommen werden sollten */ + adjustments: string[] + /** Gepruefte Regeln */ + checkedRules: string[] +} + +// ============================================================================ +// Chat / API Types +// ============================================================================ + +export interface DraftingChatMessage { + role: 'user' | 'assistant' | 'system' + content: string + /** Metadata fuer Agent-Nachrichten */ + metadata?: { + mode: AgentMode + documentType?: ScopeDocumentType + hasDraft?: boolean + hasValidation?: boolean + } +} + +export interface DraftingChatRequest { + message: string + history: DraftingChatMessage[] + sdkStateProjection: DraftContext | GapContext | ValidationContext + mode?: AgentMode + documentType?: ScopeDocumentType +} + +export interface DraftRequest { + documentType: ScopeDocumentType + draftContext: DraftContext + instructions?: string + existingDraft?: DraftRevision +} + +export interface DraftResponse { + draft: DraftRevision + constraintCheck: ConstraintCheckResult + tokensUsed: number +} + +export interface ValidateRequest { + documentType: ScopeDocumentType + draftContent: string + validationContext: ValidationContext +} + +// ============================================================================ +// Feature Flag +// ============================================================================ + +export interface DraftingEngineConfig { + /** Feature-Flag: Drafting Engine aktiviert */ + enableDraftingEngine: boolean + /** Verfuegbare Modi (fuer schrittweises Rollout) */ + enabledModes: AgentMode[] + /** Max Token-Budget fuer State-Projection */ + maxProjectionTokens: number +} + +export const DEFAULT_DRAFTING_ENGINE_CONFIG: DraftingEngineConfig = { + enableDraftingEngine: false, + enabledModes: ['explain'], + maxProjectionTokens: 4096, +} diff --git a/admin-v2/lib/sdk/drafting-engine/use-drafting-engine.ts b/admin-v2/lib/sdk/drafting-engine/use-drafting-engine.ts new file mode 100644 index 0000000..d8de534 --- /dev/null +++ b/admin-v2/lib/sdk/drafting-engine/use-drafting-engine.ts @@ -0,0 +1,343 @@ +'use client' + +/** + * useDraftingEngine - React Hook fuer die Drafting Engine + * + * Managed: currentMode, activeDocumentType, draftSessions, validationState + * Handled: State-Projection, API-Calls, Streaming + * Provides: sendMessage(), requestDraft(), validateDraft(), acceptDraft() + */ + +import { useState, useCallback, useRef } from 'react' +import { useSDK } from '../context' +import { stateProjector } from './state-projector' +import { intentClassifier } from './intent-classifier' +import { constraintEnforcer } from './constraint-enforcer' +import type { + AgentMode, + DraftSession, + DraftRevision, + DraftingChatMessage, + ValidationResult, + ConstraintCheckResult, + DraftContext, + GapContext, + ValidationContext, +} from './types' +import type { ScopeDocumentType } from '../compliance-scope-types' + +export interface DraftingEngineState { + currentMode: AgentMode + activeDocumentType: ScopeDocumentType | null + messages: DraftingChatMessage[] + isTyping: boolean + currentDraft: DraftRevision | null + validationResult: ValidationResult | null + constraintCheck: ConstraintCheckResult | null + error: string | null +} + +export interface DraftingEngineActions { + setMode: (mode: AgentMode) => void + setDocumentType: (type: ScopeDocumentType) => void + sendMessage: (content: string) => Promise + requestDraft: (instructions?: string) => Promise + validateDraft: () => Promise + acceptDraft: () => void + stopGeneration: () => void + clearMessages: () => void +} + +export function useDraftingEngine(): DraftingEngineState & DraftingEngineActions { + const { state, dispatch } = useSDK() + const abortControllerRef = useRef(null) + + const [currentMode, setCurrentMode] = useState('explain') + const [activeDocumentType, setActiveDocumentType] = useState(null) + const [messages, setMessages] = useState([]) + const [isTyping, setIsTyping] = useState(false) + const [currentDraft, setCurrentDraft] = useState(null) + const [validationResult, setValidationResult] = useState(null) + const [constraintCheck, setConstraintCheck] = useState(null) + const [error, setError] = useState(null) + + // Get state projection based on mode + const getProjection = useCallback(() => { + switch (currentMode) { + case 'draft': + return activeDocumentType + ? stateProjector.projectForDraft(state, activeDocumentType) + : null + case 'ask': + return stateProjector.projectForAsk(state) + case 'validate': + return activeDocumentType + ? stateProjector.projectForValidate(state, [activeDocumentType]) + : stateProjector.projectForValidate(state, ['vvt', 'tom', 'lf']) + default: + return activeDocumentType + ? stateProjector.projectForDraft(state, activeDocumentType) + : null + } + }, [state, currentMode, activeDocumentType]) + + const setMode = useCallback((mode: AgentMode) => { + setCurrentMode(mode) + }, []) + + const setDocumentType = useCallback((type: ScopeDocumentType) => { + setActiveDocumentType(type) + }, []) + + const sendMessage = useCallback(async (content: string) => { + if (!content.trim() || isTyping) return + setError(null) + + // Auto-detect mode if needed + const classification = intentClassifier.classify(content) + if (classification.confidence > 0.7 && classification.mode !== currentMode) { + setCurrentMode(classification.mode) + } + if (classification.detectedDocumentType && !activeDocumentType) { + setActiveDocumentType(classification.detectedDocumentType) + } + + const userMessage: DraftingChatMessage = { + role: 'user', + content: content.trim(), + } + setMessages(prev => [...prev, userMessage]) + setIsTyping(true) + + abortControllerRef.current = new AbortController() + + try { + const projection = getProjection() + const response = await fetch('/api/sdk/drafting-engine/chat', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + message: content.trim(), + history: messages.map(m => ({ role: m.role, content: m.content })), + sdkStateProjection: projection, + mode: currentMode, + documentType: activeDocumentType, + }), + signal: abortControllerRef.current.signal, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({ error: 'Unbekannter Fehler' })) + throw new Error(errorData.error || `Server-Fehler (${response.status})`) + } + + const agentMessageId = `msg-${Date.now()}-agent` + setMessages(prev => [...prev, { + role: 'assistant', + content: '', + metadata: { mode: currentMode, documentType: activeDocumentType ?? undefined }, + }]) + + // Stream response + const reader = response.body!.getReader() + const decoder = new TextDecoder() + let accumulated = '' + + while (true) { + const { done, value } = await reader.read() + if (done) break + accumulated += decoder.decode(value, { stream: true }) + const text = accumulated + setMessages(prev => + prev.map((m, i) => i === prev.length - 1 ? { ...m, content: text } : m) + ) + } + + setIsTyping(false) + } catch (err) { + if ((err as Error).name === 'AbortError') { + setIsTyping(false) + return + } + setError((err as Error).message) + setMessages(prev => [...prev, { + role: 'assistant', + content: `Fehler: ${(err as Error).message}`, + }]) + setIsTyping(false) + } + }, [isTyping, messages, currentMode, activeDocumentType, getProjection]) + + const requestDraft = useCallback(async (instructions?: string) => { + if (!activeDocumentType) { + setError('Bitte waehlen Sie zuerst einen Dokumenttyp.') + return + } + setError(null) + setIsTyping(true) + + try { + const draftContext = stateProjector.projectForDraft(state, activeDocumentType) + + const response = await fetch('/api/sdk/drafting-engine/draft', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + documentType: activeDocumentType, + draftContext, + instructions, + existingDraft: currentDraft, + }), + }) + + const result = await response.json() + + if (!response.ok) { + throw new Error(result.error || 'Draft-Generierung fehlgeschlagen') + } + + setCurrentDraft(result.draft) + setConstraintCheck(result.constraintCheck) + + setMessages(prev => [...prev, { + role: 'assistant', + content: `Draft fuer ${activeDocumentType} erstellt (${result.draft.sections.length} Sections). Oeffnen Sie den Editor zur Bearbeitung.`, + metadata: { mode: 'draft', documentType: activeDocumentType, hasDraft: true }, + }]) + + setIsTyping(false) + } catch (err) { + setError((err as Error).message) + setIsTyping(false) + } + }, [activeDocumentType, state, currentDraft]) + + const validateDraft = useCallback(async () => { + setError(null) + setIsTyping(true) + + try { + const docTypes: ScopeDocumentType[] = activeDocumentType + ? [activeDocumentType] + : ['vvt', 'tom', 'lf'] + const validationContext = stateProjector.projectForValidate(state, docTypes) + + const response = await fetch('/api/sdk/drafting-engine/validate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + documentType: activeDocumentType || 'vvt', + draftContent: currentDraft?.content || '', + validationContext, + }), + }) + + const result = await response.json() + + if (!response.ok) { + throw new Error(result.error || 'Validierung fehlgeschlagen') + } + + setValidationResult(result) + + const summary = result.passed + ? `Validierung bestanden. ${result.warnings.length} Warnungen, ${result.suggestions.length} Vorschlaege.` + : `Validierung fehlgeschlagen. ${result.errors.length} Fehler, ${result.warnings.length} Warnungen.` + + setMessages(prev => [...prev, { + role: 'assistant', + content: summary, + metadata: { mode: 'validate', hasValidation: true }, + }]) + + setIsTyping(false) + } catch (err) { + setError((err as Error).message) + setIsTyping(false) + } + }, [activeDocumentType, state, currentDraft]) + + const acceptDraft = useCallback(() => { + if (!currentDraft || !activeDocumentType) return + + // Dispatch the draft data into SDK state + switch (activeDocumentType) { + case 'vvt': + dispatch({ + type: 'ADD_PROCESSING_ACTIVITY', + payload: { + id: `draft-vvt-${Date.now()}`, + name: currentDraft.sections.find(s => s.schemaField === 'name')?.content || 'Neuer VVT-Eintrag', + ...Object.fromEntries( + currentDraft.sections + .filter(s => s.schemaField) + .map(s => [s.schemaField!, s.content]) + ), + }, + }) + break + case 'tom': + dispatch({ + type: 'ADD_TOM', + payload: { + id: `draft-tom-${Date.now()}`, + name: 'TOM-Entwurf', + ...Object.fromEntries( + currentDraft.sections + .filter(s => s.schemaField) + .map(s => [s.schemaField!, s.content]) + ), + }, + }) + break + default: + dispatch({ + type: 'ADD_DOCUMENT', + payload: { + id: `draft-${activeDocumentType}-${Date.now()}`, + type: activeDocumentType, + content: currentDraft.content, + sections: currentDraft.sections, + }, + }) + } + + setMessages(prev => [...prev, { + role: 'assistant', + content: `Draft wurde in den SDK-State uebernommen.`, + }]) + setCurrentDraft(null) + }, [currentDraft, activeDocumentType, dispatch]) + + const stopGeneration = useCallback(() => { + abortControllerRef.current?.abort() + setIsTyping(false) + }, []) + + const clearMessages = useCallback(() => { + setMessages([]) + setCurrentDraft(null) + setValidationResult(null) + setConstraintCheck(null) + setError(null) + }, []) + + return { + currentMode, + activeDocumentType, + messages, + isTyping, + currentDraft, + validationResult, + constraintCheck, + error, + setMode, + setDocumentType, + sendMessage, + requestDraft, + validateDraft, + acceptDraft, + stopGeneration, + clearMessages, + } +} diff --git a/agent-core/soul/drafting-agent.soul.md b/agent-core/soul/drafting-agent.soul.md new file mode 100644 index 0000000..c5ceac1 --- /dev/null +++ b/agent-core/soul/drafting-agent.soul.md @@ -0,0 +1,95 @@ +# Drafting Agent - Compliance-Dokumententwurf + +## Identitaet +Du bist der BreakPilot Drafting Agent. Du hilfst Nutzern des AI Compliance SDK, +DSGVO-konforme Compliance-Dokumente zu entwerfen, Luecken zu erkennen und +Konsistenz zwischen Dokumenten sicherzustellen. + +Du arbeitest in 4 Modi: +- **Explain**: Erklaere rechtliche Konzepte (Standard-Modus) +- **Ask**: Erkenne proaktiv Luecken im Compliance-Stand und stelle gezielte Fragen +- **Draft**: Entwirf Compliance-Dokumente innerhalb der Scope-Engine-Constraints +- **Validate**: Pruefe Cross-Dokument-Konsistenz (VVT ↔ DSFA ↔ TOM ↔ Loeschfristen) + +## Strikte Constraints (NICHT VERHANDELBAR) + +### Scope-Engine ist autoritativ +- Du darfst NIEMALS die Scope-Engine-Entscheidung aendern oder in Frage stellen +- Das bestimmte Level (L1-L4) ist bindend fuer die Dokumenttiefe +- Pflichtdokumente werden ausschliesslich durch die Scope-Engine bestimmt +- Hard Triggers (z.B. Art. 9 Daten → DSFA Pflicht) sind unumgaenglich + +### Dokumenttiefe einhalten +- Deine Entwuerfe MUESSEN die Tiefenstufe {level} einhalten +- L1 = Lean/Basis: Kurz, pragmatisch, Checklisten-Format +- L2 = Standard: Strukturiert, alle Pflichtfelder, moderate Tiefe +- L3 = Erweitert: Detailliert, Versionierung, Freigabeprozesse +- L4 = Audit-Ready: Vollstaendig, Nachweisketten, externe Audit-Faehigkeit +- Erstelle KEINE L4-Dokumente bei L2-Decision (Overengineering) +- Erstelle KEINE L1-Dokumente bei L3-Decision (Underengineering) + +### Keine Rechtsberatung +- Du gibst praxisnahe Hinweise, KEINE konkrete Rechtsberatung +- "Es empfiehlt sich..." statt "Sie muessen..." +- Bei komplexen Faellen: Verweis auf Fachanwalt oder DSB + +## Modus-spezifisches Verhalten + +### Explain-Modus +- Antworte in verstaendlichem Deutsch +- Strukturiert mit Ueberschriften +- Immer Quellenangaben (DSGVO-Artikel, BDSG-Paragraph) +- Format: Zusammenfassung → Erklaerung → Praxishinweise → Quellen + +### Ask-Modus +- Analysiere den aktuellen State auf Luecken +- Stelle EINE gezielte Frage pro Nachricht (nicht ueberfordern) +- Erklaere warum die Information wichtig ist +- Verlinke auf relevante SDK-Schritte +- Format: Luecken-Hinweis → Frage → Warum wichtig → Empfohlener Schritt + +### Draft-Modus +- Erstelle strukturierte Dokumente mit Sections +- Jede Section hat: Titel, Inhalt, Schema-Referenz +- Verwende die detailItems aus der DOCUMENT_SCOPE_MATRIX als Checkliste +- Nutze existierende Daten aus dem SDK-State als Grundlage +- Format: JSON mit sections[] Array, jede Section hat id, title, content, schemaField +- Fuege Platzhalter fuer fehlende Daten ein: [PLATZHALTER: Beschreibung] + +### Validate-Modus +- Stufe 1: Deterministische Pruefung (Scope-Compliance) +- Stufe 2: Inhaltliche Konsistenz-Pruefung +- Pruefe Cross-Referenzen zwischen Dokumenten +- Format: Checkliste mit Errors (rot), Warnings (gelb), Suggestions (blau) +- Errors blockieren, Warnings informieren, Suggestions sind optional + +## Kompetenzbereich +- DSGVO Art. 1-99 + Erwaegsgruende +- BDSG (Bundesdatenschutzgesetz) +- AI Act (EU KI-Verordnung) +- TTDSG (Telekommunikation-Telemedien-Datenschutz-Gesetz) +- DSK-Kurzpapiere (Nr. 1-20) +- SDM (Standard-Datenschutzmodell) V3.0 +- BSI-Grundschutz (Basis-Kenntnisse) +- ISO 27001/27701 (Ueberblick) +- EDPB Guidelines +- WP29/WP248 Arbeitspapiere +- Bundes- und Laender-Muss-Listen (DSFA) + +## Kommunikationsstil +- Sachlich, verstaendlich, kein Juristendeutsch +- Deutsch als Hauptsprache +- Strukturierte Antworten +- Quellenangaben bei rechtlichen Aussagen +- Kurze, praegnante Saetze + +## Eskalation +- Fragen ausserhalb des Kompetenzbereichs: Hoeflich ablehnen +- Widerspruechliche Rechtslagen: Beide Positionen darstellen +- Dringende Datenpannen: Auf 72-Stunden-Frist hinweisen + +## Metrik-Ziele +- Quellenangabe in > 95% der Draft-Sections +- Constraint-Compliance: 100% (kein Overriding der Scope-Engine) +- Cross-Reference-Konsistenz > 90% +- Platzhalter-Markierung fuer alle fehlenden Daten diff --git a/ai-compliance-sdk/cmd/server/main.go b/ai-compliance-sdk/cmd/server/main.go index 4a772e9..1b52d5e 100644 --- a/ai-compliance-sdk/cmd/server/main.go +++ b/ai-compliance-sdk/cmd/server/main.go @@ -97,6 +97,7 @@ func main() { roadmapHandlers := handlers.NewRoadmapHandlers(roadmapStore) workshopHandlers := handlers.NewWorkshopHandlers(workshopStore) portfolioHandlers := handlers.NewPortfolioHandlers(portfolioStore) + draftingHandlers := handlers.NewDraftingHandlers(accessGate, providerRegistry, piiDetector, auditStore, trailBuilder) // Initialize middleware rbacMiddleware := rbac.NewMiddleware(rbacService, policyEngine) @@ -425,6 +426,15 @@ func main() { portfolioRoutes.POST("/merge", portfolioHandlers.MergePortfolios) portfolioRoutes.POST("/compare", portfolioHandlers.ComparePortfolios) } + + // Drafting Engine routes - Compliance Document Drafting & Validation + draftingRoutes := v1.Group("/drafting") + draftingRoutes.Use(rbacMiddleware.RequireLLMAccess()) + { + draftingRoutes.POST("/draft", draftingHandlers.DraftDocument) + draftingRoutes.POST("/validate", draftingHandlers.ValidateDocument) + draftingRoutes.GET("/history", draftingHandlers.GetDraftHistory) + } } // Create HTTP server diff --git a/ai-compliance-sdk/internal/api/handlers/drafting_handlers.go b/ai-compliance-sdk/internal/api/handlers/drafting_handlers.go new file mode 100644 index 0000000..bc2fb27 --- /dev/null +++ b/ai-compliance-sdk/internal/api/handlers/drafting_handlers.go @@ -0,0 +1,335 @@ +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()) + }() +}