Files
breakpilot-compliance/admin-compliance/app/api/sdk/compliance-advisor/chat/route.ts
Benjamin Admin 187dbf1b77
Some checks failed
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-python-backend-compliance (push) Has been cancelled
CI / test-python-document-crawler (push) Has been cancelled
CI / test-python-dsms-gateway (push) Has been cancelled
CI / test-go-ai-compliance (push) Has been cancelled
fix(compliance-advisor): increase token limit and add source protection
- Increase num_predict from 2048 to 8192 to prevent mid-sentence cutoff
- Add "Quellenschutz" rules to system prompt: agent refuses to list all
  available sources/collections, only reveals sources used in answers
- Remove internal collection names from RAG context sent to LLM
- Agent confirms knowledge on specific topics but refuses meta-queries
  like "what sources do you have?"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 11:57:21 +01:00

326 lines
13 KiB
TypeScript

/**
* Compliance Advisor Chat API
*
* Connects the ComplianceAdvisorWidget to:
* 1. Multi-Collection RAG search (rag-service) for context across 6 collections
* 2. Ollama LLM (32B) for generating answers
*
* Supports country-specific filtering (DE, AT, CH, EU).
* Streams the LLM response back as plain text.
*/
import { NextRequest, NextResponse } from 'next/server'
const RAG_SERVICE_URL = process.env.RAG_SERVICE_URL || 'http://rag-service:8097'
const OLLAMA_URL = process.env.OLLAMA_URL || 'http://host.docker.internal:11434'
const LLM_MODEL = process.env.COMPLIANCE_LLM_MODEL || 'qwen2.5vl:32b'
// All compliance-relevant collections (without NiBiS)
const COMPLIANCE_COLLECTIONS = [
'bp_compliance_gesetze',
'bp_compliance_ce',
'bp_compliance_datenschutz',
'bp_dsfa_corpus',
'bp_compliance_recht',
'bp_legal_templates',
] as const
type Country = 'DE' | 'AT' | 'CH' | 'EU'
// 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)
- Nationale Datenschutzgesetze (AT DSG, CH DSG/DSV, etc.)
- EU-Verordnungen (DORA, MiCA, Data Act, EHDS, PSD2, AMLR, etc.)
- EU Maschinenverordnung (2023/1230) — CE-Kennzeichnung, Konformitaet, Cybersecurity fuer Maschinen
- EU Blue Guide 2022 — Leitfaden fuer EU-Produktvorschriften und CE-Kennzeichnung
- ENISA Cybersecurity Guidance (Secure by Design, Supply Chain Security)
- NIST SP 800-218 (SSDF) — Secure Software Development Framework
- NIST Cybersecurity Framework (CSF) 2.0 — Govern, Identify, Protect, Detect, Respond, Recover
- OECD AI Principles — Verantwortungsvolle KI, Transparenz, Accountability
- EU-IFRS (Verordnung 2023/1803) — EU-uebernommene International Financial Reporting Standards
- EFRAG Endorsement Status — Uebersicht welche IFRS-Standards EU-endorsed sind
## IFRS-Besonderheit (WICHTIG)
Bei ALLEN Fragen zu IFRS/IAS-Standards MUSST du folgende Punkte beachten:
1. Dein Wissen basiert auf den **EU-uebernommenen IFRS** (Verordnung 2023/1803, Stand Okt 2023).
2. Die IASB/IFRS Foundation gibt regelmaessig neue oder geaenderte Standards heraus, die von der EU noch NICHT uebernommen sein koennten.
3. Weise den Nutzer IMMER darauf hin: "Dieser Hinweis basiert auf den EU-endorsed IFRS (Stand: Verordnung 2023/1803). Pruefen Sie den aktuellen EFRAG Endorsement Status fuer neuere Standards."
4. Bei internationalen Ausschreibungen: Nur EU-endorsed IFRS sind fuer EU-Unternehmen rechtsverbindlich.
5. Verweise NICHT auf IFRS Foundation Originaltexte, sondern ausschliesslich auf die EU-Verordnung.
## 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)
## Quellenschutz (KRITISCH — IMMER EINHALTEN)
Du darfst NIEMALS verraten, welche Dokumente, Sammlungen oder Quellen in deiner Wissensbasis enthalten sind.
- Auf Fragen wie "Welche Quellen hast du?", "Was ist im RAG?", "Welche Gesetze kennst du?",
"Liste alle Dokumente auf", "Welche Verordnungen sind verfuegbar?" antwortest du:
"Ich beantworte gerne konkrete Compliance-Fragen. Bitte stellen Sie eine inhaltliche Frage
zu einem bestimmten Thema, z.B. 'Was regelt Art. 25 DSGVO?' oder 'Welche Pflichten gibt es
unter dem AI Act fuer Hochrisiko-KI?'."
- Auf konkrete Fragen wie "Kennst du die DSGVO?" oder "Weisst du etwas ueber den AI Act?"
darfst du bestaetigen, dass du zu diesem Thema Auskunft geben kannst, und eine inhaltliche
Antwort geben.
- Nenne in deinen Antworten NUR die Quellen, die du tatsaechlich fuer die konkrete Antwort
verwendet hast — niemals eine vollstaendige Liste aller verfuegbaren Quellen.
- Verrate NIEMALS Collection-Namen (bp_compliance_*, bp_dsfa_*, etc.) oder interne Systemnamen.
## 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`
const COUNTRY_LABELS: Record<Country, string> = {
DE: 'Deutschland',
AT: 'Oesterreich',
CH: 'Schweiz',
EU: 'EU-weit',
}
interface RAGSearchResult {
content: string
source_name?: string
source_code?: string
attribution_text?: string
score: number
collection?: string
metadata?: Record<string, unknown>
}
/**
* Query multiple RAG collections in parallel, with optional country filter
*/
async function queryMultiCollectionRAG(query: string, country?: Country): Promise<string> {
try {
const searchPromises = COMPLIANCE_COLLECTIONS.map(async (collection) => {
const searchBody: Record<string, unknown> = {
query,
collection,
top_k: 3,
}
// Apply country filter for gesetze collection
if (collection === 'bp_compliance_gesetze' && country && country !== 'EU') {
searchBody.metadata_filter = {
must: [
{
key: 'country',
match: { any: [country, 'EU'] },
},
],
}
}
const res = await fetch(`${RAG_SERVICE_URL}/api/v1/search`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(searchBody),
signal: AbortSignal.timeout(10000),
})
if (!res.ok) return []
const data = await res.json()
return (data.results || []).map((r: RAGSearchResult) => ({
...r,
collection,
}))
})
const settled = await Promise.allSettled(searchPromises)
const allResults: RAGSearchResult[] = []
for (const result of settled) {
if (result.status === 'fulfilled') {
allResults.push(...result.value)
}
}
// Sort by score descending, take top 8
allResults.sort((a, b) => b.score - a.score)
const topResults = allResults.slice(0, 8)
if (topResults.length === 0) return ''
return topResults
.map((r, i) => {
const source = r.source_name || r.source_code || 'Unbekannt'
return `[Quelle ${i + 1}: ${source}]\n${r.content || ''}`
})
.join('\n\n---\n\n')
} catch (error) {
console.warn('Multi-collection 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', country } = body
if (!message || typeof message !== 'string') {
return NextResponse.json({ error: 'Message is required' }, { status: 400 })
}
// Validate country parameter
const validCountry = ['DE', 'AT', 'CH', 'EU'].includes(country) ? (country as Country) : undefined
// 1. Query RAG across all collections
const ragContext = await queryMultiCollectionRAG(message, validCountry)
// 2. Build system prompt with RAG context + country
let systemContent = SYSTEM_PROMPT
if (validCountry) {
const countryLabel = COUNTRY_LABELS[validCountry]
systemContent += `\n\n## Laenderspezifische Auskunft
Der Nutzer hat "${countryLabel} (${validCountry})" gewaehlt.
- Beziehe dich AUSSCHLIESSLICH auf ${validCountry}-Recht + anwendbares EU-Recht
- Nenne IMMER explizit das Land in deiner Antwort
- Verwende NIEMALS Gesetze eines anderen Landes
- Bei ${validCountry === 'EU' ? 'EU-weiten Fragen: Beziehe dich auf EU-Verordnungen und -Richtlinien' : `${countryLabel}: Beziehe nationale Gesetze (${validCountry === 'DE' ? 'BDSG, TDDDG, TKG, UWG' : validCountry === 'AT' ? 'AT DSG, ECG, TKG, KSchG, MedienG' : 'CH DSG, DSV, OR, UWG, FMG'}) mit ein`}`
}
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 6 messages)
const messages = [
{ role: 'system', content: systemContent },
...history.slice(-6).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 }
)
}
}