5a513181cc
Completes the advisor stack (FE + orchestration; /retrieve is SDK/RAG-owned). The route
now returns the FE contract instead of a text stream:
- retrieveFull() calls /retrieve with {query, context}; consumes clarity/evidence/
visual_evidence/footnotes (exact shape per board 2026-07-01 12:25).
- mode-routing (resolveMode): clarify unless a context was chosen and /retrieve's
clarity.mode says so. clarify -> L1 general answer (completeAdvisorAnswer, ungrounded,
no sources). answer -> L2 answer over numbered evidence with [n] markers.
- citations generated here ([n] -> nth evidence unit); footnotes remapped; evidence /
visual_evidence passed through.
- advisor-llm: non-streaming completeAdvisorAnswer(). Pure mappings in retrieve-mapping.ts
(+ tests). Removed the dead v2 evidence.ts/evidence-adapter (RegulationRef moved to
regulation-display). controls-augmentation kept (tested; re-integrable later).
NOT deployed: joint deploy with the SDK /retrieve endpoint (deploy-coupling). tsc clean,
25 vitest (mapping/clarify/answer/markdown/registry/rag), check-loc 0.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
126 lines
5.0 KiB
TypeScript
126 lines
5.0 KiB
TypeScript
/**
|
|
* Compliance Advisor Chat API — Clarity-Gate orchestration.
|
|
*
|
|
* Consumes the SDK/RAG /retrieve (evidence/visual_evidence/footnotes/clarity) and returns the
|
|
* FE-facing contract (advisor-clarity-gate-contract):
|
|
* - clarify mode -> short L1 general answer (no RAG) + domain context chips
|
|
* - answer mode -> L2 answer over the scoped evidence with [n] citation markers
|
|
* Citations are generated here ([n] -> nth evidence unit). The FE renders ONLY this structured data.
|
|
*/
|
|
|
|
import { NextRequest, NextResponse } from 'next/server'
|
|
import { readSoulFile } from '@/lib/sdk/agents/soul-reader'
|
|
import { retrieveFull } from '@/lib/sdk/agents/advisor-rag'
|
|
import { completeAdvisorAnswer, type ChatMessage } from '@/lib/sdk/agents/advisor-llm'
|
|
import {
|
|
buildCitations,
|
|
mapClarity,
|
|
mapFootnotes,
|
|
numberedEvidenceForPrompt,
|
|
resolveMode,
|
|
} from '@/lib/sdk/advisor/retrieve-mapping'
|
|
import type { AdvisorResponse } from '@/lib/sdk/advisor/contract'
|
|
|
|
type Country = 'DE' | 'AT' | 'CH' | 'EU'
|
|
|
|
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]
|
|
return `\n\n## Laenderspezifische Auskunft
|
|
Der Nutzer hat "${label} (${c})" gewaehlt. Beziehe dich auf ${c}-Recht + anwendbares EU-Recht und nenne das Land.`
|
|
}
|
|
|
|
// L1: general knowledge, deliberately NOT grounded (the clarify step precedes the legal retrieval).
|
|
const L1_SYSTEM = `Du bist der BreakPilot Compliance-Berater. Gib eine KURZE, allgemeine Definition/Erklaerung
|
|
des gefragten Begriffs aus Allgemeinwissen — 2 bis 4 Saetze, Markdown, neutral. NENNE KEINE Rechtsquellen,
|
|
Paragraphen, Artikel oder Fundstellen; der Nutzer waehlt anschliessend einen konkreten Kontext, erst dann
|
|
folgen belegte Quellen. Wenn der Begriff in mehreren Bereichen vorkommt, erwaehne das in einem Halbsatz.`
|
|
|
|
const FALLBACK_SYSTEM = `Du bist der BreakPilot Compliance-Berater. Antworte quellenbasiert, verstaendlich und ehrlich auf Deutsch.`
|
|
|
|
function answerSystem(soul: string | null, country: Country | undefined, evidenceBlock: string): string {
|
|
let s = soul || FALLBACK_SYSTEM
|
|
if (country) s += countryBlock(country)
|
|
s += `\n\n## Belegte Evidence (nummeriert — DEINE EINZIGEN Quellen)\n${evidenceBlock || '(keine Evidence gefunden)'}`
|
|
s += `\n\n## Antwortformat (WICHTIG)
|
|
- Gut gegliedertes Markdown: kurze ## Ueberschriften je Aspekt, Aufzaehlungen, **Fettung** fuer Kernbegriffe.
|
|
- Belege Kernaussagen mit [n], wobei n die NUMMER der Evidence-Quelle oben ist (z. B. [1], [2]).
|
|
- Nenne KEINE Quellen-/Fundstellen-Liste im Fliesstext — die Quellen werden dem Nutzer separat angezeigt.
|
|
- Triff KEINE Aussage, die nicht durch die nummerierte Evidence belegt ist; fehlt der Beleg, sage das offen.`
|
|
return s
|
|
}
|
|
|
|
export async function POST(request: NextRequest) {
|
|
try {
|
|
const body = await request.json()
|
|
const question = String(body.question ?? body.message ?? '').trim()
|
|
const context: string | null = body.context ?? null
|
|
const country = (['DE', 'AT', 'CH', 'EU'] as const).includes(body.country)
|
|
? (body.country as Country)
|
|
: undefined
|
|
|
|
if (!question) {
|
|
return NextResponse.json({ error: 'Question is required' }, { status: 400 })
|
|
}
|
|
|
|
const retrieved = await retrieveFull(question, context)
|
|
const mode = resolveMode(retrieved.clarity?.mode, !!context)
|
|
|
|
if (mode === 'clarify') {
|
|
const general = await completeAdvisorAnswer([
|
|
{ role: 'system', content: L1_SYSTEM },
|
|
{ role: 'user', content: question },
|
|
])
|
|
if (general === null) {
|
|
return NextResponse.json({ error: 'LLM nicht erreichbar.' }, { status: 502 })
|
|
}
|
|
const resp: AdvisorResponse = {
|
|
mode: 'clarify',
|
|
question,
|
|
clarity: mapClarity(retrieved.clarity, 'clarify'),
|
|
general_answer: general,
|
|
answer: null,
|
|
scoped_query: null,
|
|
evidence: [],
|
|
citations: [],
|
|
visual_evidence: [],
|
|
footnotes: [],
|
|
}
|
|
return NextResponse.json(resp)
|
|
}
|
|
|
|
const evidence = retrieved.evidence ?? []
|
|
const soul = await readSoulFile('compliance-advisor')
|
|
const messages: ChatMessage[] = [
|
|
{ role: 'system', content: answerSystem(soul, country, numberedEvidenceForPrompt(evidence)) },
|
|
{ role: 'user', content: question },
|
|
]
|
|
const answer = await completeAdvisorAnswer(messages)
|
|
if (answer === null) {
|
|
return NextResponse.json({ error: 'LLM nicht erreichbar.' }, { status: 502 })
|
|
}
|
|
const resp: AdvisorResponse = {
|
|
mode: 'answer',
|
|
question,
|
|
clarity: mapClarity(retrieved.clarity, 'answer'),
|
|
general_answer: null,
|
|
answer,
|
|
scoped_query: context,
|
|
evidence,
|
|
citations: buildCitations(evidence),
|
|
visual_evidence: retrieved.visual_evidence ?? [],
|
|
footnotes: mapFootnotes(retrieved.footnotes),
|
|
}
|
|
return NextResponse.json(resp)
|
|
} catch (error) {
|
|
console.error('Compliance advisor chat error:', error)
|
|
return NextResponse.json({ error: 'Verbindung zum Advisor fehlgeschlagen.' }, { status: 503 })
|
|
}
|
|
}
|