Initial commit: breakpilot-compliance - Compliance SDK Platform
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>
This commit is contained in:
184
admin-compliance/app/api/sdk/drafting-engine/chat/route.ts
Normal file
184
admin-compliance/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-compliance/app/api/sdk/drafting-engine/draft/route.ts
Normal file
168
admin-compliance/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-compliance/app/api/sdk/drafting-engine/validate/route.ts
Normal file
188
admin-compliance/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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user