Services: Admin-Compliance, Backend-Compliance, AI-Compliance-SDK, Consent-SDK, Developer-Portal, PCA-Platform, DSMS Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
169 lines
5.6 KiB
TypeScript
169 lines
5.6 KiB
TypeScript
/**
|
|
* 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 }
|
|
)
|
|
}
|
|
}
|