feat(advisor): evidence-framed header + bindingness contract seam
Rework the Compliance Advisor header ("Diese Antwort stuetzt sich auf")
to describe the EVIDENCE rather than the documents: binding
Rechtsgrundlagen split from Leitlinien (soft-law guidance), a
per-regulation breakdown, plus Abbildungen, Fussnoten and Evidence Units.
No fabricated trust score — objective counts only.
- bindingness is a canonical Legal-KG fact (APEX rule): added an optional
EvidenceUnit.bindingness contract seam; the FE renders the split from it
and degrades to a neutral per-regulation breakdown when it is absent
(SDK/RAG asked via board to populate it in /retrieve).
- evidence-grouping.ts: pure, tested grouping/counting model.
- route.ts: optional `audience` field (tonality) kept out of the retrieval
question; answers lead with a "Kurz gesagt" summary, structured by theme.
- E2E + unit tests updated for the evidence framing.
Not deployed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -45,17 +45,26 @@ folgen belegte Quellen. Wenn der Begriff in mehreren Bereichen vorkommt, erwaehn
|
||||
|
||||
const FALLBACK_SYSTEM = `Du bist der BreakPilot Compliance-Berater. Antworte quellenbasiert, verstaendlich und ehrlich auf Deutsch.`
|
||||
|
||||
// Optional audience/tonality guidance (e.g. the workspace's role hint). Kept out of the retrieval
|
||||
// `question` on purpose — it only shapes the answer's tone, so it belongs in the system prompt.
|
||||
function audienceBlock(audience: string): string {
|
||||
return audience ? `\n\n## Ansprache / Zielgruppe\n${audience}` : ''
|
||||
}
|
||||
|
||||
function answerSystem(
|
||||
soul: string | null,
|
||||
country: Country | undefined,
|
||||
evidenceBlock: string,
|
||||
withCitations = true,
|
||||
audience = '',
|
||||
): string {
|
||||
let s = soul || FALLBACK_SYSTEM
|
||||
if (country) s += countryBlock(country)
|
||||
s += audienceBlock(audience)
|
||||
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.`
|
||||
- Beginne mit einer **Kurzzusammenfassung** (1–2 Saetze, "Kurz gesagt: …"), die den Kern direkt beantwortet.
|
||||
- Danach gut gegliedertes Markdown: kurze ## Ueberschriften je THEMA/Aspekt (nicht je Rechtsquelle), Aufzaehlungen, **Fettung** fuer Kernbegriffe.`
|
||||
if (withCitations) {
|
||||
s += `\n- 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.`
|
||||
@@ -71,6 +80,7 @@ export async function POST(request: NextRequest) {
|
||||
const body = await request.json()
|
||||
const question = String(body.question ?? body.message ?? '').trim()
|
||||
const context: string | null = body.context ?? null
|
||||
const audience = typeof body.audience === 'string' ? body.audience.trim() : ''
|
||||
const country = (['DE', 'AT', 'CH', 'EU'] as const).includes(body.country)
|
||||
? (body.country as Country)
|
||||
: undefined
|
||||
@@ -87,7 +97,7 @@ export async function POST(request: NextRequest) {
|
||||
const legacyEvidence = retrieved.evidence ?? []
|
||||
const legacySoul = await readSoulFile('compliance-advisor')
|
||||
const legacyStream = await streamAdvisorAnswer([
|
||||
{ role: 'system', content: answerSystem(legacySoul, country, numberedEvidenceForPrompt(legacyEvidence), false) },
|
||||
{ role: 'system', content: answerSystem(legacySoul, country, numberedEvidenceForPrompt(legacyEvidence), false, audience) },
|
||||
{ role: 'user', content: question },
|
||||
])
|
||||
if (!legacyStream) {
|
||||
@@ -106,7 +116,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
if (mode === 'clarify') {
|
||||
const general = await completeAdvisorAnswer([
|
||||
{ role: 'system', content: L1_SYSTEM },
|
||||
{ role: 'system', content: L1_SYSTEM + audienceBlock(audience) },
|
||||
{ role: 'user', content: question },
|
||||
])
|
||||
if (general === null) {
|
||||
@@ -130,7 +140,7 @@ export async function POST(request: NextRequest) {
|
||||
const evidence = retrieved.evidence ?? []
|
||||
const soul = await readSoulFile('compliance-advisor')
|
||||
const messages: ChatMessage[] = [
|
||||
{ role: 'system', content: answerSystem(soul, country, numberedEvidenceForPrompt(evidence)) },
|
||||
{ role: 'system', content: answerSystem(soul, country, numberedEvidenceForPrompt(evidence), true, audience) },
|
||||
{ role: 'user', content: question },
|
||||
]
|
||||
const answer = await completeAdvisorAnswer(messages)
|
||||
|
||||
Reference in New Issue
Block a user