feat(sdk): Add Drafting Engine with 4-mode agent system (Explain/Ask/Draft/Validate)
Extends the Compliance Advisor from a Q&A chatbot into a full drafting engine that can generate, validate, and refine compliance documents within Scope Engine constraints. Includes intent classifier, state projector, constraint enforcer, SOUL templates, Go backend endpoints, and React UI components. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
184
admin-v2/app/api/sdk/drafting-engine/chat/route.ts
Normal file
184
admin-v2/app/api/sdk/drafting-engine/chat/route.ts
Normal file
@@ -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<string> {
|
||||
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<string, string> = {
|
||||
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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
168
admin-v2/app/api/sdk/drafting-engine/draft/route.ts
Normal file
168
admin-v2/app/api/sdk/drafting-engine/draft/route.ts
Normal file
@@ -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<string, unknown>, 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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
188
admin-v2/app/api/sdk/drafting-engine/validate/route.ts
Normal file
188
admin-v2/app/api/sdk/drafting-engine/validate/route.ts
Normal file
@@ -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<string, unknown>, 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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ interface Message {
|
||||
|
||||
interface ComplianceAdvisorWidgetProps {
|
||||
currentStep?: string
|
||||
enableDraftingEngine?: boolean
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -58,7 +59,13 @@ const EXAMPLE_QUESTIONS: Record<string, string[]> = {
|
||||
// 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 <DraftingEngineWidget currentStep={currentStep} enableDraftingEngine />
|
||||
}
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [isExpanded, setIsExpanded] = useState(false)
|
||||
const [messages, setMessages] = useState<Message[]>([])
|
||||
|
||||
300
admin-v2/components/sdk/DraftEditor.tsx
Normal file
300
admin-v2/components/sdk/DraftEditor.tsx
Normal file
@@ -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<string | null>(null)
|
||||
const contentRef = useRef<HTMLDivElement>(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 (
|
||||
<div className="fixed inset-0 bg-gray-900/50 z-50 flex items-center justify-center p-4">
|
||||
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-6xl h-[85vh] flex flex-col overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="bg-gradient-to-r from-blue-600 to-indigo-600 text-white px-6 py-3 flex items-center justify-between shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="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" />
|
||||
</svg>
|
||||
<div>
|
||||
<div className="font-semibold text-sm">{docLabel} - Entwurf</div>
|
||||
<div className="text-xs text-white/70">
|
||||
{draft.sections.length} Sections | Erstellt {new Date(draft.createdAt).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Constraint Badge */}
|
||||
{constraintCheck && (
|
||||
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${
|
||||
constraintCheck.allowed
|
||||
? 'bg-green-500/20 text-green-100'
|
||||
: 'bg-red-500/20 text-red-100'
|
||||
}`}>
|
||||
{constraintCheck.allowed ? 'Constraints OK' : 'Constraint-Verletzung'}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Validation Badge */}
|
||||
{validationResult && (
|
||||
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${
|
||||
validationResult.passed
|
||||
? 'bg-green-500/20 text-green-100'
|
||||
: 'bg-amber-500/20 text-amber-100'
|
||||
}`}>
|
||||
{validationResult.passed ? 'Validiert' : `${validationResult.errors.length} Fehler`}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="text-white/80 hover:text-white transition-colors p-1"
|
||||
aria-label="Editor schliessen"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Adjustment Warnings */}
|
||||
{constraintCheck && constraintCheck.adjustments.length > 0 && (
|
||||
<div className="px-6 py-2 bg-amber-50 border-b border-amber-200 shrink-0">
|
||||
{constraintCheck.adjustments.map((adj, i) => (
|
||||
<p key={i} className="text-xs text-amber-700 flex items-start gap-1">
|
||||
<svg className="w-3.5 h-3.5 mt-0.5 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="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" />
|
||||
</svg>
|
||||
{adj}
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main Content: 2/3 Editor + 1/3 Chat */}
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
{/* Left: Draft Content (2/3) */}
|
||||
<div className="w-2/3 border-r border-gray-200 overflow-y-auto" ref={contentRef}>
|
||||
{/* Section Navigation */}
|
||||
<div className="sticky top-0 bg-white border-b border-gray-100 px-4 py-2 flex items-center gap-1 overflow-x-auto z-10">
|
||||
{draft.sections.map((section) => (
|
||||
<button
|
||||
key={section.id}
|
||||
onClick={() => {
|
||||
setActiveSection(section.id)
|
||||
document.getElementById(`section-${section.id}`)?.scrollIntoView({ behavior: 'smooth' })
|
||||
}}
|
||||
className={`px-2.5 py-1 rounded-md text-xs font-medium whitespace-nowrap transition-colors ${
|
||||
activeSection === section.id
|
||||
? 'bg-blue-100 text-blue-700'
|
||||
: 'text-gray-500 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
{section.title}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Sections */}
|
||||
<div className="p-6 space-y-6">
|
||||
{draft.sections.map((section) => (
|
||||
<div
|
||||
key={section.id}
|
||||
id={`section-${section.id}`}
|
||||
className={`rounded-lg border transition-colors ${
|
||||
activeSection === section.id
|
||||
? 'border-blue-300 bg-blue-50/30'
|
||||
: 'border-gray-200 bg-white'
|
||||
}`}
|
||||
>
|
||||
<div className="px-4 py-2.5 border-b border-gray-100 flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold text-gray-900">{section.title}</h3>
|
||||
{section.schemaField && (
|
||||
<span className="text-xs text-gray-400 font-mono">{section.schemaField}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="px-4 py-3">
|
||||
<div className="text-sm text-gray-700 whitespace-pre-wrap leading-relaxed">
|
||||
{section.content}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: Refinement Chat (1/3) */}
|
||||
<div className="w-1/3 flex flex-col bg-gray-50">
|
||||
<div className="px-4 py-3 border-b border-gray-200 bg-white">
|
||||
<h3 className="text-sm font-semibold text-gray-900">Verfeinerung</h3>
|
||||
<p className="text-xs text-gray-500">Geben Sie Anweisungen zur Verbesserung</p>
|
||||
</div>
|
||||
|
||||
{/* Validation Summary (if present) */}
|
||||
{validationResult && (
|
||||
<div className="px-4 py-3 border-b border-gray-100">
|
||||
<div className="space-y-1.5">
|
||||
{validationResult.errors.length > 0 && (
|
||||
<div className="flex items-center gap-1.5 text-xs text-red-600">
|
||||
<span className="w-2 h-2 rounded-full bg-red-500" />
|
||||
{validationResult.errors.length} Fehler
|
||||
</div>
|
||||
)}
|
||||
{validationResult.warnings.length > 0 && (
|
||||
<div className="flex items-center gap-1.5 text-xs text-amber-600">
|
||||
<span className="w-2 h-2 rounded-full bg-amber-500" />
|
||||
{validationResult.warnings.length} Warnungen
|
||||
</div>
|
||||
)}
|
||||
{validationResult.suggestions.length > 0 && (
|
||||
<div className="flex items-center gap-1.5 text-xs text-blue-600">
|
||||
<span className="w-2 h-2 rounded-full bg-blue-500" />
|
||||
{validationResult.suggestions.length} Vorschlaege
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Refinement Area */}
|
||||
<div className="flex-1 p-4 overflow-y-auto">
|
||||
<div className="space-y-3">
|
||||
<p className="text-xs text-gray-500">
|
||||
Beschreiben Sie, was geaendert werden soll. Der Agent erstellt eine ueberarbeitete Version unter Beachtung der Scope-Constraints.
|
||||
</p>
|
||||
|
||||
{/* Quick Refinement Buttons */}
|
||||
<div className="space-y-1.5">
|
||||
{[
|
||||
'Mehr Details hinzufuegen',
|
||||
'Platzhalter ausfuellen',
|
||||
'Rechtliche Referenzen ergaenzen',
|
||||
'Sprache vereinfachen',
|
||||
].map((suggestion) => (
|
||||
<button
|
||||
key={suggestion}
|
||||
onClick={() => onRefine(suggestion)}
|
||||
disabled={isTyping}
|
||||
className="w-full text-left px-3 py-1.5 text-xs bg-white hover:bg-blue-50 border border-gray-200 rounded-md transition-colors text-gray-600 disabled:opacity-50"
|
||||
>
|
||||
{suggestion}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Refinement Input */}
|
||||
<div className="border-t border-gray-200 p-3 bg-white">
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={refineInput}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<button
|
||||
onClick={handleRefine}
|
||||
disabled={!refineInput.trim() || isTyping}
|
||||
className="px-3 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer Actions */}
|
||||
<div className="border-t border-gray-200 px-6 py-3 bg-white flex items-center justify-between shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={onValidate}
|
||||
disabled={isTyping}
|
||||
className="px-4 py-2 text-sm font-medium text-green-700 bg-green-50 border border-green-200 rounded-lg hover:bg-green-100 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
Validieren
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-sm font-medium text-gray-600 bg-gray-50 border border-gray-200 rounded-lg hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
onClick={onAccept}
|
||||
disabled={isTyping}
|
||||
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
Draft akzeptieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
417
admin-v2/components/sdk/DraftingEngineWidget.tsx
Normal file
417
admin-v2/components/sdk/DraftingEngineWidget.tsx
Normal file
@@ -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<AgentMode, { label: string; color: string; activeColor: string; icon: string }> = {
|
||||
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<AgentMode, string[]> = {
|
||||
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<HTMLDivElement>(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 (
|
||||
<button
|
||||
onClick={() => setIsOpen(true)}
|
||||
className="fixed bottom-6 right-[5.5rem] w-14 h-14 bg-indigo-600 hover:bg-indigo-700 text-white rounded-full shadow-lg flex items-center justify-center transition-all duration-200 hover:scale-110 z-50"
|
||||
aria-label="Drafting Engine oeffnen"
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="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" />
|
||||
</svg>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
// Draft Editor full-screen overlay
|
||||
if (showDraftEditor && engine.currentDraft) {
|
||||
return (
|
||||
<DraftEditor
|
||||
draft={engine.currentDraft}
|
||||
documentType={engine.activeDocumentType}
|
||||
constraintCheck={engine.constraintCheck}
|
||||
onAccept={() => {
|
||||
engine.acceptDraft()
|
||||
setShowDraftEditor(false)
|
||||
}}
|
||||
onValidate={() => {
|
||||
engine.validateDraft()
|
||||
}}
|
||||
onClose={() => setShowDraftEditor(false)}
|
||||
onRefine={(instruction: string) => {
|
||||
engine.requestDraft(instruction)
|
||||
}}
|
||||
validationResult={engine.validationResult}
|
||||
isTyping={engine.isTyping}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`fixed bottom-6 right-6 ${isExpanded ? 'w-[700px] h-[80vh]' : 'w-[420px] h-[560px]'} max-h-screen bg-white rounded-2xl shadow-2xl flex flex-col z-50 border border-gray-200 transition-all duration-200`}>
|
||||
{/* Header */}
|
||||
<div className="bg-gradient-to-r from-purple-600 to-indigo-600 text-white px-4 py-3 rounded-t-2xl flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 bg-white/20 rounded-full flex items-center justify-center">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="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" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-semibold text-sm">Drafting Engine</div>
|
||||
<div className="text-xs text-white/80">Compliance-Dokumententwurf</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="text-white/80 hover:text-white transition-colors p-1"
|
||||
aria-label={isExpanded ? 'Verkleinern' : 'Vergroessern'}
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
{isExpanded ? (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 9L4 4m0 0v4m0-4h4m6 6l5 5m0 0v-4m0 4h-4" />
|
||||
) : (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 8V4m0 0h4M4 4l5 5m11-1V4m0 0h-4m4 0l-5 5M4 16v4m0 0h4m-4 0l5-5m11 5v-4m0 4h-4m4 0l-5-5" />
|
||||
)}
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
engine.clearMessages()
|
||||
setIsOpen(false)
|
||||
}}
|
||||
className="text-white/80 hover:text-white transition-colors p-1"
|
||||
aria-label="Schliessen"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mode Pills */}
|
||||
<div className="flex items-center gap-1 px-3 py-2 border-b border-gray-100 bg-white">
|
||||
{(Object.keys(MODE_CONFIG) as AgentMode[]).map((mode) => {
|
||||
const config = MODE_CONFIG[mode]
|
||||
const isActive = engine.currentMode === mode
|
||||
return (
|
||||
<button
|
||||
key={mode}
|
||||
onClick={() => engine.setMode(mode)}
|
||||
className={`flex items-center gap-1 px-2.5 py-1 rounded-full text-xs font-medium transition-all ${isActive ? config.activeColor : config.color} hover:opacity-80`}
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={config.icon} />
|
||||
</svg>
|
||||
{config.label}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Document Type Selector (visible in draft/validate mode) */}
|
||||
{(engine.currentMode === 'draft' || engine.currentMode === 'validate') && (
|
||||
<div className="px-3 py-2 border-b border-gray-100 bg-gray-50/50">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-gray-500 shrink-0">Dokument:</span>
|
||||
<select
|
||||
value={engine.activeDocumentType || ''}
|
||||
onChange={(e) => engine.setDocumentType(e.target.value as ScopeDocumentType)}
|
||||
className="flex-1 text-xs border border-gray-200 rounded-md px-2 py-1 bg-white focus:outline-none focus:ring-1 focus:ring-purple-400"
|
||||
>
|
||||
<option value="">Dokumenttyp waehlen...</option>
|
||||
{availableDocumentTypes.map((dt) => (
|
||||
<option key={dt} value={dt}>
|
||||
{DOCUMENT_TYPE_LABELS[dt] || dt}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error Banner */}
|
||||
{engine.error && (
|
||||
<div className="mx-3 mt-2 px-3 py-2 bg-red-50 border border-red-200 rounded-lg text-xs text-red-700 flex items-center justify-between">
|
||||
<span>{engine.error}</span>
|
||||
<button onClick={() => engine.clearMessages()} className="text-red-500 hover:text-red-700 ml-2">
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Validation Report Inline */}
|
||||
{showValidationReport && engine.validationResult && (
|
||||
<div className="mx-3 mt-2 max-h-48 overflow-y-auto">
|
||||
<ValidationReport
|
||||
result={engine.validationResult}
|
||||
onClose={() => setShowValidationReport(false)}
|
||||
compact
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Messages Area */}
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-3 bg-gray-50">
|
||||
{engine.messages.length === 0 ? (
|
||||
<div className="text-center py-6">
|
||||
<div className="w-14 h-14 bg-purple-100 rounded-full flex items-center justify-center mx-auto mb-3">
|
||||
<svg className="w-7 h-7 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={MODE_CONFIG[engine.currentMode].icon} />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-sm font-medium text-gray-900 mb-1">
|
||||
{engine.currentMode === 'explain' && 'Fragen beantworten'}
|
||||
{engine.currentMode === 'ask' && 'Luecken erkennen'}
|
||||
{engine.currentMode === 'draft' && 'Dokumente entwerfen'}
|
||||
{engine.currentMode === 'validate' && 'Konsistenz pruefen'}
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500 mb-4">
|
||||
{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.'}
|
||||
</p>
|
||||
|
||||
<div className="text-left space-y-2">
|
||||
<p className="text-xs font-medium text-gray-700 mb-2">Beispiele:</p>
|
||||
{exampleQuestions.map((q, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
onClick={() => handleSendMessage(q)}
|
||||
className="w-full text-left px-3 py-2 text-xs bg-white hover:bg-purple-50 border border-gray-200 rounded-lg transition-colors text-gray-700"
|
||||
>
|
||||
{q}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Quick Actions for Draft/Validate */}
|
||||
{engine.currentMode === 'draft' && engine.activeDocumentType && (
|
||||
<button
|
||||
onClick={() => engine.requestDraft()}
|
||||
className="mt-4 px-4 py-2 bg-blue-600 text-white text-xs font-medium rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
Draft fuer {DOCUMENT_TYPE_LABELS[engine.activeDocumentType]?.split(' (')[0] || engine.activeDocumentType} erstellen
|
||||
</button>
|
||||
)}
|
||||
{engine.currentMode === 'validate' && (
|
||||
<button
|
||||
onClick={() => engine.validateDraft()}
|
||||
className="mt-4 px-4 py-2 bg-green-600 text-white text-xs font-medium rounded-lg hover:bg-green-700 transition-colors"
|
||||
>
|
||||
Validierung starten
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{engine.messages.map((message, idx) => (
|
||||
<div
|
||||
key={idx}
|
||||
className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
|
||||
>
|
||||
<div
|
||||
className={`max-w-[85%] rounded-lg px-3 py-2 ${
|
||||
message.role === 'user'
|
||||
? 'bg-indigo-600 text-white'
|
||||
: 'bg-white border border-gray-200 text-gray-800'
|
||||
}`}
|
||||
>
|
||||
<p className={`text-sm ${message.role === 'assistant' ? 'whitespace-pre-wrap' : ''}`}>
|
||||
{message.content}
|
||||
</p>
|
||||
|
||||
{/* Draft ready indicator */}
|
||||
{message.metadata?.hasDraft && engine.currentDraft && (
|
||||
<button
|
||||
onClick={() => setShowDraftEditor(true)}
|
||||
className="mt-2 flex items-center gap-1.5 px-3 py-1.5 bg-blue-50 border border-blue-200 rounded-md text-xs text-blue-700 hover:bg-blue-100 transition-colors"
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||
</svg>
|
||||
Im Editor oeffnen
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Validation ready indicator */}
|
||||
{message.metadata?.hasValidation && engine.validationResult && (
|
||||
<button
|
||||
onClick={() => setShowValidationReport(true)}
|
||||
className="mt-2 flex items-center gap-1.5 px-3 py-1.5 bg-green-50 border border-green-200 rounded-md text-xs text-green-700 hover:bg-green-100 transition-colors"
|
||||
>
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
Validierungsbericht anzeigen
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{engine.isTyping && (
|
||||
<div className="flex justify-start">
|
||||
<div className="bg-white border border-gray-200 rounded-lg px-3 py-2">
|
||||
<div className="flex space-x-1">
|
||||
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" />
|
||||
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.1s' }} />
|
||||
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{ animationDelay: '0.2s' }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div ref={messagesEndRef} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Input Area */}
|
||||
<div className="border-t border-gray-200 p-3 bg-white rounded-b-2xl">
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={inputValue}
|
||||
onChange={(e) => 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 ? (
|
||||
<button
|
||||
onClick={engine.stopGeneration}
|
||||
className="px-3 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors"
|
||||
title="Generierung stoppen"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 6h12v12H6z" />
|
||||
</svg>
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => handleSendMessage(inputValue)}
|
||||
disabled={!inputValue.trim()}
|
||||
className="px-3 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
220
admin-v2/components/sdk/ValidationReport.tsx
Normal file
220
admin-v2/components/sdk/ValidationReport.tsx
Normal file
@@ -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 (
|
||||
<div className={`flex items-start gap-2 px-2.5 py-1.5 ${config.bg} rounded-md border ${config.border}`}>
|
||||
<span className={`w-1.5 h-1.5 rounded-full mt-1.5 shrink-0 ${config.dotColor}`} />
|
||||
<div className="min-w-0">
|
||||
<p className={`text-xs font-medium ${config.text}`}>{finding.title}</p>
|
||||
<p className="text-xs text-gray-500 truncate">{finding.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`${config.bg} rounded-lg border ${config.border} p-3`}>
|
||||
<div className="flex items-start gap-2">
|
||||
<svg className={`w-4 h-4 mt-0.5 shrink-0 ${config.text}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={config.icon} />
|
||||
</svg>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className={`text-sm font-medium ${config.text}`}>{finding.title}</h4>
|
||||
<span className="text-xs text-gray-400 bg-gray-100 px-1.5 py-0.5 rounded">{docLabel}</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-600 mt-1">{finding.description}</p>
|
||||
|
||||
{finding.crossReferenceType && (
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Cross-Referenz: {DOCUMENT_TYPE_LABELS[finding.crossReferenceType]?.split(' (')[0] || finding.crossReferenceType}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{finding.legalReference && (
|
||||
<p className="text-xs text-gray-500 mt-1 font-mono">{finding.legalReference}</p>
|
||||
)}
|
||||
|
||||
{finding.suggestion && (
|
||||
<div className="mt-2 flex items-start gap-1.5 px-2.5 py-1.5 bg-white/60 rounded border border-gray-100">
|
||||
<svg className="w-3.5 h-3.5 mt-0.5 text-gray-400 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
|
||||
</svg>
|
||||
<p className="text-xs text-gray-600">{finding.suggestion}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ValidationReport({ result, onClose, compact }: ValidationReportProps) {
|
||||
const totalFindings = result.errors.length + result.warnings.length + result.suggestions.length
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
<div className="rounded-lg border border-gray-200 bg-white overflow-hidden">
|
||||
<div className="flex items-center justify-between px-3 py-2 bg-gray-50 border-b border-gray-100">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`w-2 h-2 rounded-full ${result.passed ? 'bg-green-500' : 'bg-red-500'}`} />
|
||||
<span className="text-xs font-medium text-gray-700">
|
||||
{result.passed ? 'Validierung bestanden' : 'Validierung fehlgeschlagen'}
|
||||
</span>
|
||||
<span className="text-xs text-gray-400">
|
||||
({totalFindings} {totalFindings === 1 ? 'Fund' : 'Funde'})
|
||||
</span>
|
||||
</div>
|
||||
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">
|
||||
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-2 space-y-1.5 max-h-36 overflow-y-auto">
|
||||
{result.errors.map((f) => <FindingCard key={f.id} finding={f} compact />)}
|
||||
{result.warnings.map((f) => <FindingCard key={f.id} finding={f} compact />)}
|
||||
{result.suggestions.map((f) => <FindingCard key={f.id} finding={f} compact />)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Summary Header */}
|
||||
<div className={`rounded-lg border p-4 ${result.passed ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200'}`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${result.passed ? 'bg-green-100' : 'bg-red-100'}`}>
|
||||
<svg className={`w-5 h-5 ${result.passed ? 'text-green-600' : 'text-red-600'}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={result.passed ? 'M5 13l4 4L19 7' : 'M6 18L18 6M6 6l12 12'} />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className={`text-sm font-semibold ${result.passed ? 'text-green-800' : 'text-red-800'}`}>
|
||||
{result.passed ? 'Validierung bestanden' : 'Validierung fehlgeschlagen'}
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500">
|
||||
Level {result.scopeLevel} | {new Date(result.timestamp).toLocaleString('de-DE')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="flex items-center gap-3">
|
||||
{result.errors.length > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="w-2 h-2 rounded-full bg-red-500" />
|
||||
<span className="text-xs font-medium text-red-700">{result.errors.length}</span>
|
||||
</div>
|
||||
)}
|
||||
{result.warnings.length > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="w-2 h-2 rounded-full bg-amber-500" />
|
||||
<span className="text-xs font-medium text-amber-700">{result.warnings.length}</span>
|
||||
</div>
|
||||
)}
|
||||
{result.suggestions.length > 0 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="w-2 h-2 rounded-full bg-blue-500" />
|
||||
<span className="text-xs font-medium text-blue-700">{result.suggestions.length}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button onClick={onClose} className="text-gray-400 hover:text-gray-600 ml-2">
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Errors */}
|
||||
{result.errors.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold text-red-700 uppercase tracking-wide mb-2">
|
||||
Fehler ({result.errors.length})
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{result.errors.map((f) => <FindingCard key={f.id} finding={f} />)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Warnings */}
|
||||
{result.warnings.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold text-amber-700 uppercase tracking-wide mb-2">
|
||||
Warnungen ({result.warnings.length})
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{result.warnings.map((f) => <FindingCard key={f.id} finding={f} />)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Suggestions */}
|
||||
{result.suggestions.length > 0 && (
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold text-blue-700 uppercase tracking-wide mb-2">
|
||||
Vorschlaege ({result.suggestions.length})
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{result.suggestions.map((f) => <FindingCard key={f.id} finding={f} />)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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> = {}): 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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> = {}): 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
|
||||
})
|
||||
})
|
||||
})
|
||||
221
admin-v2/lib/sdk/drafting-engine/constraint-enforcer.ts
Normal file
221
admin-v2/lib/sdk/drafting-engine/constraint-enforcer.ts
Normal file
@@ -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()
|
||||
241
admin-v2/lib/sdk/drafting-engine/intent-classifier.ts
Normal file
241
admin-v2/lib/sdk/drafting-engine/intent-classifier.ts
Normal file
@@ -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()
|
||||
49
admin-v2/lib/sdk/drafting-engine/prompts/ask-gap-analysis.ts
Normal file
49
admin-v2/lib/sdk/drafting-engine/prompts/ask-gap-analysis.ts
Normal file
@@ -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`
|
||||
}
|
||||
91
admin-v2/lib/sdk/drafting-engine/prompts/draft-dsfa.ts
Normal file
91
admin-v2/lib/sdk/drafting-engine/prompts/draft-dsfa.ts
Normal file
@@ -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.`
|
||||
}
|
||||
@@ -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}.`
|
||||
}
|
||||
102
admin-v2/lib/sdk/drafting-engine/prompts/draft-privacy-policy.ts
Normal file
102
admin-v2/lib/sdk/drafting-engine/prompts/draft-privacy-policy.ts
Normal file
@@ -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}.`
|
||||
}
|
||||
99
admin-v2/lib/sdk/drafting-engine/prompts/draft-tom.ts
Normal file
99
admin-v2/lib/sdk/drafting-engine/prompts/draft-tom.ts
Normal file
@@ -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}.`
|
||||
}
|
||||
109
admin-v2/lib/sdk/drafting-engine/prompts/draft-vvt.ts
Normal file
109
admin-v2/lib/sdk/drafting-engine/prompts/draft-vvt.ts
Normal file
@@ -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}).`
|
||||
}
|
||||
11
admin-v2/lib/sdk/drafting-engine/prompts/index.ts
Normal file
11
admin-v2/lib/sdk/drafting-engine/prompts/index.ts
Normal file
@@ -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'
|
||||
@@ -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": [...]
|
||||
}`
|
||||
}
|
||||
337
admin-v2/lib/sdk/drafting-engine/state-projector.ts
Normal file
337
admin-v2/lib/sdk/drafting-engine/state-projector.ts
Normal file
@@ -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<string, unknown>).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<string, unknown>).name) : ''
|
||||
).filter(Boolean),
|
||||
retentionCategories: (state.retentionPolicies ?? []).map(p =>
|
||||
typeof p === 'object' && p !== null && 'name' in p ? String((p as Record<string, unknown>).name) : ''
|
||||
).filter(Boolean),
|
||||
}
|
||||
|
||||
// Depth-Requirements fuer alle angefragten Typen
|
||||
const depthRequirements: Record<string, DocumentDepthRequirement> = {}
|
||||
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<ScopeDocumentType, DocumentDepthRequirement>,
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// 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<string, unknown> | 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()
|
||||
279
admin-v2/lib/sdk/drafting-engine/types.ts
Normal file
279
admin-v2/lib/sdk/drafting-engine/types.ts
Normal file
@@ -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<string, unknown>
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 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<string, unknown>
|
||||
}>
|
||||
/** 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<ScopeDocumentType, DocumentDepthRequirement>
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 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,
|
||||
}
|
||||
343
admin-v2/lib/sdk/drafting-engine/use-drafting-engine.ts
Normal file
343
admin-v2/lib/sdk/drafting-engine/use-drafting-engine.ts
Normal file
@@ -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<void>
|
||||
requestDraft: (instructions?: string) => Promise<void>
|
||||
validateDraft: () => Promise<void>
|
||||
acceptDraft: () => void
|
||||
stopGeneration: () => void
|
||||
clearMessages: () => void
|
||||
}
|
||||
|
||||
export function useDraftingEngine(): DraftingEngineState & DraftingEngineActions {
|
||||
const { state, dispatch } = useSDK()
|
||||
const abortControllerRef = useRef<AbortController | null>(null)
|
||||
|
||||
const [currentMode, setCurrentMode] = useState<AgentMode>('explain')
|
||||
const [activeDocumentType, setActiveDocumentType] = useState<ScopeDocumentType | null>(null)
|
||||
const [messages, setMessages] = useState<DraftingChatMessage[]>([])
|
||||
const [isTyping, setIsTyping] = useState(false)
|
||||
const [currentDraft, setCurrentDraft] = useState<DraftRevision | null>(null)
|
||||
const [validationResult, setValidationResult] = useState<ValidationResult | null>(null)
|
||||
const [constraintCheck, setConstraintCheck] = useState<ConstraintCheckResult | null>(null)
|
||||
const [error, setError] = useState<string | null>(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,
|
||||
}
|
||||
}
|
||||
95
agent-core/soul/drafting-agent.soul.md
Normal file
95
agent-core/soul/drafting-agent.soul.md
Normal file
@@ -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
|
||||
@@ -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
|
||||
|
||||
335
ai-compliance-sdk/internal/api/handlers/drafting_handlers.go
Normal file
335
ai-compliance-sdk/internal/api/handlers/drafting_handlers.go
Normal file
@@ -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())
|
||||
}()
|
||||
}
|
||||
Reference in New Issue
Block a user