Remove duplicate compliance and DSGVO admin pages that have been superseded by the unified SDK pipeline. Update navigation, sidebar, roles, and module registry to reflect the new structure. Add DSFA corpus API proxy and source-policy components. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
226 lines
7.7 KiB
TypeScript
226 lines
7.7 KiB
TypeScript
/**
|
|
* Compliance Advisor Chat API
|
|
*
|
|
* Connects the ComplianceAdvisorWidget to:
|
|
* 1. RAG legal corpus search (klausur-service) for context
|
|
* 2. Ollama LLM (32B) for generating answers
|
|
*
|
|
* Streams the LLM response back as plain text.
|
|
*/
|
|
|
|
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/compliance-advisor.soul.md)
|
|
const SYSTEM_PROMPT = `# Compliance Advisor Agent
|
|
|
|
## Identitaet
|
|
Du bist der BreakPilot Compliance-Berater. Du hilfst Nutzern des AI Compliance SDK,
|
|
Datenschutz- und Compliance-Fragen in verstaendlicher Sprache zu beantworten.
|
|
Du bist kein Anwalt und gibst keine Rechtsberatung, sondern orientierst dich an
|
|
offiziellen Quellen und gibst praxisnahe Hinweise.
|
|
|
|
## Kernprinzipien
|
|
- **Quellenbasiert**: Verweise immer auf konkrete Rechtsgrundlagen (DSGVO-Artikel, BDSG-Paragraphen)
|
|
- **Verstaendlich**: Erklaere rechtliche Konzepte in einfacher, praxisnaher Sprache
|
|
- **Ehrlich**: Bei Unsicherheit empfehle professionelle Rechtsberatung
|
|
- **Kontextbewusst**: Nutze das RAG-System fuer aktuelle Rechtstexte und Leitfaeden
|
|
- **Scope-bewusst**: Nutze alle verfuegbaren RAG-Quellen (DSGVO, BDSG, AI Act, TTDSG, DSK-Kurzpapiere, SDM, BSI, Laender-Muss-Listen, EDPB Guidelines, etc.) AUSSER NIBIS-Dokumenten.
|
|
|
|
## Kompetenzbereich
|
|
- DSGVO Art. 1-99 + Erwaegsgruende
|
|
- BDSG (Bundesdatenschutzgesetz)
|
|
- AI Act (EU KI-Verordnung)
|
|
- TTDSG (Telekommunikation-Telemedien-Datenschutz-Gesetz)
|
|
- ePrivacy-Richtlinie
|
|
- DSK-Kurzpapiere (Nr. 1-20)
|
|
- SDM (Standard-Datenschutzmodell) V3.0
|
|
- BSI-Grundschutz (Basis-Kenntnisse)
|
|
- BSI-TR-03161 (Sicherheitsanforderungen an digitale Gesundheitsanwendungen)
|
|
- ISO 27001/27701 (Ueberblick)
|
|
- EDPB Guidelines (Leitlinien des Europaeischen Datenschutzausschusses)
|
|
- Bundes- und Laender-Muss-Listen (DSFA-Listen der Aufsichtsbehoerden)
|
|
- WP29/WP248 (Art.-29-Datenschutzgruppe Arbeitspapiere)
|
|
|
|
## RAG-Nutzung
|
|
Nutze das gesamte RAG-Corpus fuer Kontext und Quellenangaben — ausgenommen sind
|
|
NIBIS-Inhalte (Erwartungshorizonte, Bildungsstandards, curriculare Vorgaben).
|
|
Diese gehoeren nicht zum Datenschutz-Kompetenzbereich.
|
|
|
|
## Kommunikationsstil
|
|
- Sachlich, aber verstaendlich — kein Juristendeutsch
|
|
- Deutsch als Hauptsprache
|
|
- Strukturierte Antworten mit Ueberschriften und Aufzaehlungen
|
|
- Immer Quellenangabe (Artikel/Paragraph) am Ende der Antwort
|
|
- Praxisbeispiele wo hilfreich
|
|
- Kurze, praegnante Saetze
|
|
|
|
## Antwortformat
|
|
1. Kurze Zusammenfassung (1-2 Saetze)
|
|
2. Detaillierte Erklaerung
|
|
3. Praxishinweise / Handlungsempfehlungen
|
|
4. Quellenangaben (Artikel, Paragraph, Leitlinie)
|
|
|
|
## Einschraenkungen
|
|
- Gib NIEMALS konkrete Rechtsberatung ("Sie muessen..." -> "Es empfiehlt sich...")
|
|
- Keine Garantien fuer Rechtssicherheit
|
|
- Bei komplexen Einzelfaellen: Empfehle Rechtsanwalt/DSB
|
|
- Keine Aussagen zu laufenden Verfahren oder Bussgeldern
|
|
- Keine Interpretation von Urteilen (nur Verweis)
|
|
|
|
## Eskalation
|
|
- Bei Fragen ausserhalb des Kompetenzbereichs: Hoeflich ablehnen und auf Fachanwalt verweisen
|
|
- Bei widerspruechlichen Rechtslagen: Beide Positionen darstellen und DSB-Konsultation empfehlen
|
|
- Bei dringenden Datenpannen: Auf 72-Stunden-Frist (Art. 33 DSGVO) hinweisen und Notfallplan-Modul empfehlen`
|
|
|
|
interface RAGResult {
|
|
content: string
|
|
source_name: string
|
|
source_code: string
|
|
attribution_text: string
|
|
score: number
|
|
}
|
|
|
|
/**
|
|
* Query the DSFA 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=5`
|
|
const res = await fetch(url, {
|
|
headers: { 'Content-Type': 'application/json' },
|
|
signal: AbortSignal.timeout(10000),
|
|
})
|
|
|
|
if (!res.ok) {
|
|
console.warn('RAG search failed:', res.status)
|
|
return ''
|
|
}
|
|
|
|
const data = await res.json()
|
|
|
|
if (data.results && data.results.length > 0) {
|
|
return data.results
|
|
.map(
|
|
(r: RAGResult, i: number) =>
|
|
`[Quelle ${i + 1}: ${r.source_name || r.source_code || 'Unbekannt'}]\n${r.content || ''}`
|
|
)
|
|
.join('\n\n---\n\n')
|
|
}
|
|
|
|
return ''
|
|
} catch (error) {
|
|
console.warn('RAG query error (continuing without context):', error)
|
|
return ''
|
|
}
|
|
}
|
|
|
|
export async function POST(request: NextRequest) {
|
|
try {
|
|
const body = await request.json()
|
|
const { message, history = [], currentStep = 'default' } = body
|
|
|
|
if (!message || typeof message !== 'string') {
|
|
return NextResponse.json({ error: 'Message is required' }, { status: 400 })
|
|
}
|
|
|
|
// 1. Query RAG for relevant context
|
|
const ragContext = await queryRAG(message)
|
|
|
|
// 2. Build system prompt with RAG context
|
|
let systemContent = SYSTEM_PROMPT
|
|
|
|
if (ragContext) {
|
|
systemContent += `\n\n## Relevanter Kontext aus dem RAG-System\n\nNutze die folgenden Quellen fuer deine Antwort. Verweise in deiner Antwort auf die jeweilige Quelle:\n\n${ragContext}`
|
|
}
|
|
|
|
systemContent += `\n\n## Aktueller SDK-Schritt\nDer Nutzer befindet sich im SDK-Schritt: ${currentStep}`
|
|
|
|
// 3. Build messages array (limit history to last 10 messages)
|
|
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 Ollama 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: 0.3,
|
|
num_predict: 8192,
|
|
},
|
|
}),
|
|
signal: AbortSignal.timeout(120000),
|
|
})
|
|
|
|
if (!ollamaResponse.ok) {
|
|
const errorText = await ollamaResponse.text()
|
|
console.error('Ollama error:', ollamaResponse.status, errorText)
|
|
return NextResponse.json(
|
|
{ error: `LLM nicht erreichbar (Status ${ollamaResponse.status}). Ist Ollama mit dem Modell ${LLM_MODEL} gestartet?` },
|
|
{ status: 502 }
|
|
)
|
|
}
|
|
|
|
// 5. Stream response back as plain text
|
|
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 line, skip
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Stream read 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('Compliance advisor chat error:', error)
|
|
return NextResponse.json(
|
|
{ error: 'Verbindung zum LLM fehlgeschlagen. Bitte pruefen Sie ob Ollama laeuft.' },
|
|
{ status: 503 }
|
|
)
|
|
}
|
|
}
|