33085c61b4
Authority-/Freshness-Layer Punkte 1/2/5 im Advisor-Antwortpfad (Prompt-Ebene, kein Schema). Neue Soul-Sektion "Korpus-Autoritaet & Aktualitaet": rechtliche FAKTEN (Schwellen/Fristen/Zahlen/Pflichten) nur aus bereitgestelltem RAG-/Controls-Kontext, Trainingswissen nie als Rechtsquelle; Konflikt -> Kontext gewinnt, transparent; Co-Pilot-Ton statt Roboter-Verweigerung. Ergaenzt Quellentreue (Fundstellen) um die Fakten-Ebene -> loest den "DSB ab 10 statt 20"-Fall. route.ts: RAG-Framing als "deine EINZIGEN Rechtsquellen" verschaerft. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
122 lines
4.5 KiB
TypeScript
122 lines
4.5 KiB
TypeScript
/**
|
|
* Compliance Advisor Chat API
|
|
*
|
|
* Verbindet das ComplianceAdvisorWidget mit:
|
|
* 1. Multi-Collection-RAG ueber die ai-compliance-sdk (bge-m3) — siehe advisor-rag
|
|
* 2. Strukturierten Controls zum erkannten Thema — buildControlsContext
|
|
* 3. LLM-Kaskade OVH (prod) -> Ollama (Dev) — siehe advisor-llm
|
|
*
|
|
* Laenderspezifische Filterung (DE, AT, CH, EU). Streamt die Antwort als Text.
|
|
*/
|
|
|
|
import { NextRequest, NextResponse } from 'next/server'
|
|
import { readSoulFile } from '@/lib/sdk/agents/soul-reader'
|
|
import { buildControlsContext } from '@/lib/sdk/agents/controls-augmentation'
|
|
import { queryAdvisorRAG } from '@/lib/sdk/agents/advisor-rag'
|
|
import { streamAdvisorAnswer, type ChatMessage } from '@/lib/sdk/agents/advisor-llm'
|
|
|
|
type Country = 'DE' | 'AT' | 'CH' | 'EU'
|
|
|
|
const FALLBACK_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.
|
|
|
|
## Kernprinzipien
|
|
- Quellenbasiert: Verweise auf DSGVO-Artikel, BDSG-Paragraphen
|
|
- Verstaendlich: Einfache, praxisnahe Sprache
|
|
- Ehrlich: Bei Unsicherheit empfehle Rechtsberatung
|
|
- Deutsch als Hauptsprache`
|
|
|
|
const COUNTRY_LABELS: Record<Country, string> = {
|
|
DE: 'Deutschland',
|
|
AT: 'Oesterreich',
|
|
CH: 'Schweiz',
|
|
EU: 'EU-weit',
|
|
}
|
|
|
|
function countryBlock(c: Country): string {
|
|
const label = COUNTRY_LABELS[c]
|
|
const nationalLaws =
|
|
c === 'DE'
|
|
? 'BDSG, TDDDG, TKG, UWG'
|
|
: c === 'AT'
|
|
? 'AT DSG, ECG, TKG, KSchG, MedienG'
|
|
: 'CH DSG, DSV, OR, UWG, FMG'
|
|
const guidance =
|
|
c === 'EU'
|
|
? 'EU-weiten Fragen: Beziehe dich auf EU-Verordnungen und -Richtlinien'
|
|
: `${label}: Beziehe nationale Gesetze (${nationalLaws}) mit ein`
|
|
return `\n\n## Laenderspezifische Auskunft
|
|
Der Nutzer hat "${label} (${c})" gewaehlt.
|
|
- Beziehe dich AUSSCHLIESSLICH auf ${c}-Recht + anwendbares EU-Recht
|
|
- Nenne IMMER explizit das Land in deiner Antwort
|
|
- Verwende NIEMALS Gesetze eines anderen Landes
|
|
- Bei ${guidance}`
|
|
}
|
|
|
|
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 })
|
|
}
|
|
|
|
const validCountry = (['DE', 'AT', 'CH', 'EU'] as const).includes(country)
|
|
? (country as Country)
|
|
: undefined
|
|
|
|
// 1. RAG (ai-sdk, bge-m3) + strukturierte Controls zum Thema — beide parallel
|
|
const [ragContext, controlsContext] = await Promise.all([
|
|
queryAdvisorRAG(message),
|
|
buildControlsContext(message),
|
|
])
|
|
|
|
// 2. System-Prompt zusammenbauen
|
|
const soulPrompt = await readSoulFile('compliance-advisor')
|
|
let systemContent = soulPrompt || FALLBACK_SYSTEM_PROMPT
|
|
if (validCountry) systemContent += countryBlock(validCountry)
|
|
if (ragContext) {
|
|
systemContent += `\n\n## Relevanter Kontext aus dem RAG-System (deine EINZIGEN Rechtsquellen)\n\nDies sind deine einzigen zulaessigen Rechtsquellen. Triff keine konkrete Rechtsaussage (Zahl, Frist, Schwelle, Pflicht, Fundstelle), die nicht hier oder im Controls-Block belegt ist — sonst sage offen, dass du sie aus deinen Quellen nicht belegen kannst. Verweise in deiner Antwort auf die jeweilige Quelle:\n\n${ragContext}`
|
|
}
|
|
if (controlsContext) systemContent += `\n\n${controlsContext}`
|
|
systemContent += `\n\n## Aktueller SDK-Schritt\nDer Nutzer befindet sich im SDK-Schritt: ${currentStep}`
|
|
|
|
// 3. Nachrichten (History auf die letzten 6 begrenzen)
|
|
const messages: ChatMessage[] = [
|
|
{ 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. LLM-Kaskade -> Plain-Text-Stream
|
|
const stream = await streamAdvisorAnswer(messages)
|
|
if (!stream) {
|
|
return NextResponse.json(
|
|
{ error: 'LLM nicht erreichbar. Weder OVH/LiteLLM noch Ollama haben geantwortet.' },
|
|
{ status: 502 },
|
|
)
|
|
}
|
|
|
|
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.' },
|
|
{ status: 503 },
|
|
)
|
|
}
|
|
}
|