/** * 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) ## 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 = { 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 } /** * Query multiple RAG collections in parallel, with optional country filter */ async function queryMultiCollectionRAG(query: string, country?: Country): Promise { try { const searchPromises = COMPLIANCE_COLLECTIONS.map(async (collection) => { const searchBody: Record = { 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' const col = r.collection ? ` [${r.collection}]` : '' return `[Quelle ${i + 1}: ${source}${col}]\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: 2048, }, }), 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 } ) } }