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 516b9f43..0bea8949 100644 --- a/admin-compliance/app/api/sdk/compliance-advisor/chat/route.ts +++ b/admin-compliance/app/api/sdk/compliance-advisor/chat/route.ts @@ -11,9 +11,10 @@ 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 { completeAdvisorAnswer, streamAdvisorAnswer, type ChatMessage } from '@/lib/sdk/agents/advisor-llm' import { buildCitations, + isLegacyRequest, mapClarity, mapFootnotes, numberedEvidenceForPrompt, @@ -44,15 +45,24 @@ 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.` -function answerSystem(soul: string | null, country: Country | undefined, evidenceBlock: string): string { +function answerSystem( + soul: string | null, + country: Country | undefined, + evidenceBlock: string, + withCitations = true, +): 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.` +- Gut gegliedertes Markdown: kurze ## Ueberschriften je Aspekt, 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.` + } else { + s += `\n- Nenne Fundstellen nur, wo sie der Antwort dienen (natuerlich im Text, KEIN [n]-Markup).` + } + s += `\n- Triff KEINE Aussage, die nicht durch die nummerierte Evidence belegt ist; fehlt der Beleg, sage das offen.` return s } @@ -70,6 +80,28 @@ export async function POST(request: NextRequest) { } const retrieved = await retrieveFull(question, context) + + // Backward-compat: legacy consumers (breakpilot-workspace) send {message} and read a plain-text + // stream. Serve the L2 answer streamed (clean prose, no [n]); no clarify gate, no JSON. + if (isLegacyRequest(body)) { + const legacyEvidence = retrieved.evidence ?? [] + const legacySoul = await readSoulFile('compliance-advisor') + const legacyStream = await streamAdvisorAnswer([ + { role: 'system', content: answerSystem(legacySoul, country, numberedEvidenceForPrompt(legacyEvidence), false) }, + { role: 'user', content: question }, + ]) + if (!legacyStream) { + return NextResponse.json({ error: 'LLM nicht erreichbar.' }, { status: 502 }) + } + return new NextResponse(legacyStream, { + headers: { + 'Content-Type': 'text/plain; charset=utf-8', + 'Cache-Control': 'no-cache', + 'X-Advisor-Format': 'legacy-stream', + }, + }) + } + const mode = resolveMode(retrieved.clarity?.mode, !!context) if (mode === 'clarify') { diff --git a/admin-compliance/lib/sdk/__tests__/advisor-retrieve-mapping.test.ts b/admin-compliance/lib/sdk/__tests__/advisor-retrieve-mapping.test.ts index 71e60932..24cb8f60 100644 --- a/admin-compliance/lib/sdk/__tests__/advisor-retrieve-mapping.test.ts +++ b/admin-compliance/lib/sdk/__tests__/advisor-retrieve-mapping.test.ts @@ -5,6 +5,7 @@ import { mapFootnotes, buildCitations, numberedEvidenceForPrompt, + isLegacyRequest, } from '../advisor/retrieve-mapping' import type { EvidenceUnit } from '../advisor/contract' @@ -68,3 +69,13 @@ describe('mapFootnotes', () => { }) }) }) + +describe('isLegacyRequest', () => { + it('message-only (workspace) -> legacy stream', () => { + expect(isLegacyRequest({ message: 'Ist meine DSE ausreichend?' })).toBe(true) + }) + it('question present -> contract (JSON)', () => { + expect(isLegacyRequest({ question: 'x', message: 'y' })).toBe(false) + expect(isLegacyRequest({ question: 'x' })).toBe(false) + }) +}) diff --git a/admin-compliance/lib/sdk/advisor/retrieve-mapping.ts b/admin-compliance/lib/sdk/advisor/retrieve-mapping.ts index 187a3a92..e036445f 100644 --- a/admin-compliance/lib/sdk/advisor/retrieve-mapping.ts +++ b/admin-compliance/lib/sdk/advisor/retrieve-mapping.ts @@ -81,3 +81,11 @@ export function numberedEvidenceForPrompt(evidence: EvidenceUnit[]): string { }) .join('\n\n') } + +/** + * Backward-compat discriminator: legacy consumers (e.g. breakpilot-workspace) send `{message}` + * and read a plain-text stream; the new FE sends `{question}` and expects the JSON contract. + */ +export function isLegacyRequest(body: { question?: unknown; message?: unknown }): boolean { + return body.question == null && typeof body.message === 'string' +}