/** * Compliance-Advisor RAG-Suche. * * Fragt den Authority Router der ai-compliance-sdk (`/sdk/v1/rag/retrieve`) mit NUR der * Query ab — der Router waehlt selbst die Collections (Broad-Authority-Base + KB-2026.1-Slice * bei in-scope), embeddet mit bge-m3 (prod: ollama-embed), merged + authority-ranked. Der * Advisor bleibt damit collection-agnostisch (Vertrag: Compiler -> Collections -> Retriever * -> Advisor); die fruehere Multi-Collection-Logik liegt jetzt im Retriever. * * `retrieveAdvisorEvidence` liefert die STRUKTURIERTEN Treffer (fuer das Evidence-Workspace- * Frontend, das nur strukturierte Daten rendert und nie den Antworttext parst) UND den * vorformatierten Kontext-Block fuer den LLM-Prompt. Fehler werden geschluckt (graceful). */ const SDK_URL = process.env.SDK_API_URL || process.env.SDK_URL || 'http://ai-compliance-sdk:8090' const DEFAULT_USER = '00000000-0000-0000-0000-000000000001' const DEFAULT_TENANT = process.env.DEFAULT_TENANT_ID || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e' export interface SdkRagResult { text?: string regulation_code?: string regulation_name?: string regulation_short?: string article_label?: string article?: string paragraph?: string sub?: string citation_style?: string is_recital?: boolean category?: string source_url?: string score?: number } /** Raw RAG response. `figures`/`footnotes` (C8 / C-FN) are passed through untyped until the * RAG-ingestion contract is finalized (board), then mapped in the evidence-adapter. */ interface SdkRagResponse { results?: SdkRagResult[] figures?: unknown[] footnotes?: unknown[] } interface ScoredPassage { content: string source: string score: number } /** Normalisiert eine ai-sdk-RAG-Antwort auf {content, source, score} (fuer den Prompt-Kontext). */ export function mapSdkResults(results: SdkRagResult[] | undefined): ScoredPassage[] { return (results || []) .map((r) => ({ content: r.text || '', // Fundstelle: article_label ist die fertig formatierte, druckbare Quelle aus der // Ingestion ("BDSG § 38 Abs. 1"); Fallback baut sie aus den strukturierten Feldern. source: (r.article_label && r.article_label.trim()) || [r.regulation_short || r.regulation_name || r.regulation_code, r.article, r.paragraph, r.sub] .filter(Boolean) .join(' ') || 'Unbekannt', score: typeof r.score === 'number' ? r.score : 0, })) .filter((p) => p.content) } /** Formatiert die Top-Passagen als Kontext-Block fuer den System-Prompt. */ function formatContext(passages: ScoredPassage[]): string { if (passages.length === 0) return '' return passages .map((r, i) => `[Quelle ${i + 1}: ${r.source}]\n${r.content}`) .join('\n\n---\n\n') } /** EIN collection-agnostischer Aufruf an die ai-sdk. Fehler -> leeres Ergebnis (graceful). */ async function fetchRag(query: string): Promise { try { const res = await fetch(`${SDK_URL}/sdk/v1/rag/retrieve`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-User-ID': DEFAULT_USER, 'X-Tenant-ID': DEFAULT_TENANT, }, body: JSON.stringify({ query, top_k: 8 }), signal: AbortSignal.timeout(15000), }) if (res.ok) return ((await res.json()) as SdkRagResponse) || {} } catch { // graceful: keine Verbindung -> Antwort ohne RAG-Kontext } return {} } export interface AdvisorEvidenceRaw { contextText: string results: SdkRagResult[] figures?: unknown[] footnotes?: unknown[] } /** * Strukturierte Evidence + Prompt-Kontext aus EINEM Retrieval. Das Frontend bekommt die * `results` (und kuenftig `figures`/`footnotes`) als Daten; der `contextText` geht in den * LLM-Prompt. Reihenfolge der authority-geordneten Top-K bleibt erhalten. */ export async function retrieveAdvisorEvidence(query: string): Promise { const data = await fetchRag(query) const results = data.results || [] return { contextText: formatContext(mapSdkResults(results)), results, figures: Array.isArray(data.figures) ? data.figures : undefined, footnotes: Array.isArray(data.footnotes) ? data.footnotes : undefined, } } /** Abwaertskompatibel: nur der Prompt-Kontext als String. */ export async function queryAdvisorRAG(query: string): Promise { return (await retrieveAdvisorEvidence(query)).contextText }