From 0e932c0df883a4c8e03ca64f7d5573a4d1bac063 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Sat, 28 Feb 2026 01:04:30 +0100 Subject: [PATCH] feat(advisor): multi-collection RAG search + country filter (DE/AT/CH/EU) - Replace single DSFA corpus query with parallel search across 6 collections via RAG service (port 8097) - Add country parameter with metadata filter for bp_compliance_gesetze - Add country-specific system prompt section - Add DE/AT/CH/EU toggle buttons in ComplianceAdvisorWidget header Co-Authored-By: Claude Opus 4.6 --- .../api/sdk/compliance-advisor/chat/route.ts | 133 ++++++++++++++---- .../sdk/ComplianceAdvisorWidget.tsx | 29 +++- 2 files changed, 129 insertions(+), 33 deletions(-) diff --git a/admin-compliance/app/api/sdk/compliance-advisor/chat/route.ts b/admin-compliance/app/api/sdk/compliance-advisor/chat/route.ts index 9cb618a..66e72bd 100644 --- a/admin-compliance/app/api/sdk/compliance-advisor/chat/route.ts +++ b/admin-compliance/app/api/sdk/compliance-advisor/chat/route.ts @@ -2,18 +2,31 @@ * Compliance Advisor Chat API * * Connects the ComplianceAdvisorWidget to: - * 1. RAG legal corpus search (klausur-service) for context + * 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 KLAUSUR_SERVICE_URL = process.env.KLAUSUR_SERVICE_URL || 'http://klausur-service:8086' +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 @@ -44,6 +57,8 @@ offiziellen Quellen und gibst praxisnahe Hinweise. - 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.) ## RAG-Nutzung Nutze das gesamte RAG-Corpus fuer Kontext und Quellenangaben — ausgenommen sind @@ -76,44 +91,87 @@ Diese gehoeren nicht zum Datenschutz-Kompetenzbereich. - 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 { +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 + source_name?: string + source_code?: string + attribution_text?: string score: number + collection?: string + metadata?: Record } /** - * Query the DSFA RAG corpus for relevant documents + * Query multiple RAG collections in parallel, with optional country filter */ -async function queryRAG(query: string): Promise { +async function queryMultiCollectionRAG(query: string, country?: Country): Promise { 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), + 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, + })) }) - if (!res.ok) { - console.warn('RAG search failed:', res.status) - return '' + const settled = await Promise.allSettled(searchPromises) + const allResults: RAGSearchResult[] = [] + + for (const result of settled) { + if (result.status === 'fulfilled') { + allResults.push(...result.value) + } } - const data = await res.json() + // Sort by score descending, take top 8 + allResults.sort((a, b) => b.score - a.score) + const topResults = allResults.slice(0, 8) - 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') - } + if (topResults.length === 0) return '' - 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('RAG query error (continuing without context):', error) + console.warn('Multi-collection RAG query error (continuing without context):', error) return '' } } @@ -121,25 +179,38 @@ async function queryRAG(query: string): Promise { export async function POST(request: NextRequest) { try { const body = await request.json() - const { message, history = [], currentStep = 'default' } = body + const { message, history = [], currentStep = 'default', country } = 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) + // Validate country parameter + const validCountry = ['DE', 'AT', 'CH', 'EU'].includes(country) ? (country as Country) : undefined - // 2. Build system prompt with RAG context + // 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 10 messages) + // 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 }) => ({ diff --git a/admin-compliance/components/sdk/ComplianceAdvisorWidget.tsx b/admin-compliance/components/sdk/ComplianceAdvisorWidget.tsx index 65b1eab..5fb92ce 100644 --- a/admin-compliance/components/sdk/ComplianceAdvisorWidget.tsx +++ b/admin-compliance/components/sdk/ComplianceAdvisorWidget.tsx @@ -59,6 +59,15 @@ const EXAMPLE_QUESTIONS: Record = { // COMPONENT // ============================================================================= +type Country = 'DE' | 'AT' | 'CH' | 'EU' + +const COUNTRIES: { code: Country; label: string }[] = [ + { code: 'DE', label: 'DE' }, + { code: 'AT', label: 'AT' }, + { code: 'CH', label: 'CH' }, + { code: 'EU', label: 'EU' }, +] + export function ComplianceAdvisorWidget({ currentStep = 'default', enableDraftingEngine = false }: ComplianceAdvisorWidgetProps) { // Feature-flag: If Drafting Engine enabled, render DraftingEngineWidget instead if (enableDraftingEngine) { @@ -71,6 +80,7 @@ export function ComplianceAdvisorWidget({ currentStep = 'default', enableDraftin const [messages, setMessages] = useState([]) const [inputValue, setInputValue] = useState('') const [isTyping, setIsTyping] = useState(false) + const [selectedCountry, setSelectedCountry] = useState('DE') const messagesEndRef = useRef(null) const abortControllerRef = useRef(null) @@ -124,6 +134,7 @@ export function ComplianceAdvisorWidget({ currentStep = 'default', enableDraftin message: content.trim(), history, currentStep, + country: selectedCountry, }), signal: abortControllerRef.current.signal, }) @@ -201,7 +212,7 @@ export function ComplianceAdvisorWidget({ currentStep = 'default', enableDraftin setIsTyping(false) } }, - [isTyping, messages, currentStep] + [isTyping, messages, currentStep, selectedCountry] ) // Handle stop generation @@ -269,7 +280,21 @@ export function ComplianceAdvisorWidget({ currentStep = 'default', enableDraftin
Compliance Advisor
-
KI-gestuetzter Assistent
+
+ {COUNTRIES.map(({ code, label }) => ( + + ))} +