// Adapter: RAG/compiler output -> the structured AdvisorEvidenceMeta the Evidence Workspace renders. // This is the ONLY place that maps backend shapes to the frontend envelope. The frontend never // parses the answer text — all structure originates here from structured fields. import type { AdvisorEvidenceMeta, FigureUnit, FootnoteUnit, KnowledgeUnit, RegulationRef, } from './evidence' import { deriveStats } from './evidence' import type { SdkRagResult } from '../agents/advisor-rag' /** Provisional raw figure (C8) shape — reconcile with the RAG-ingestion contract (board). */ export interface RawFigure { figure_id?: string id?: string label?: string // "Abbildung 3" caption?: string topic?: string regulation_code?: string regulation_short?: string regulation_name?: string section?: string vision_summary?: string description?: string image_url?: string url?: string } /** Provisional raw footnote (C-FN) shape — reconcile with the RAG-ingestion contract (board). */ export interface RawFootnote { id?: string ref?: string number?: string | number regulation_code?: string regulation_short?: string regulation_name?: string section?: string text?: string } export interface RawEvidenceInput { results?: SdkRagResult[] figures?: RawFigure[] footnotes?: RawFootnote[] } function regulationRef( code?: string, name?: string, short?: string, ): RegulationRef { return { code: (code || short || name || 'unknown').toLowerCase().replace(/\s+/g, '_'), name: name || undefined, short: short || name || code || 'Quelle', } } function truncate(text: string, max = 240): string { const t = text.trim().replace(/\s+/g, ' ') return t.length > max ? `${t.slice(0, max - 1)}…` : t } function toKnowledgeUnit(r: SdkRagResult, idx: number): KnowledgeUnit | null { const regulation = regulationRef(r.regulation_code, r.regulation_name, r.regulation_short) const section = r.is_recital ? `Erwägungsgrund ${r.article ?? ''}`.trim() : r.article || undefined const label = r.article_label?.trim() || undefined // Drop empty placeholders: a unit needs at least a label or a section to be meaningful. if (!label && !section && !regulation.name && regulation.short === 'Quelle') return null return { id: `src-${idx}`, regulation, section, paragraph: r.paragraph || undefined, subsection: r.sub || undefined, label, score: typeof r.score === 'number' ? r.score : undefined, snippet: r.text ? truncate(r.text) : undefined, open: r.source_url ? { originalUrl: r.source_url } : undefined, } } function dedupeKey(u: KnowledgeUnit): string { return [u.regulation.code, u.section, u.paragraph, u.subsection, u.label] .map((x) => x || '') .join('|') } function toFigureUnit(f: RawFigure, idx: number): FigureUnit | null { const id = f.figure_id || f.id const imageUrl = f.image_url || f.url if (!id && !imageUrl && !f.label) return null return { id: id || `fig-${idx}`, label: f.label || `Abbildung ${idx + 1}`, caption: f.caption || undefined, topic: f.topic || undefined, source: regulationRef(f.regulation_code, f.regulation_name, f.regulation_short), section: f.section || undefined, visionSummary: f.vision_summary || f.description || undefined, imageUrl: imageUrl || undefined, } } function toFootnoteUnit(f: RawFootnote, idx: number): FootnoteUnit | null { const ref = f.ref || (f.number != null ? `Fußnote ${f.number}` : undefined) if (!ref && !f.text) return null return { id: f.id || `fn-${idx}`, ref: ref || `Fußnote ${idx + 1}`, source: regulationRef(f.regulation_code, f.regulation_name, f.regulation_short), section: f.section || undefined, text: f.text || undefined, } } /** * Build the structured evidence meta. Sources are deduped (same citation retrieved multiple * times collapses to one, keeping the highest score) and order is preserved by score. */ export function adaptEvidence(input: RawEvidenceInput): AdvisorEvidenceMeta { const seen = new Map() ;(input.results || []).forEach((r, i) => { const unit = toKnowledgeUnit(r, i) if (!unit) return const key = dedupeKey(unit) const existing = seen.get(key) if (!existing || (unit.score ?? 0) > (existing.score ?? 0)) seen.set(key, unit) }) const sources = [...seen.values()].sort((a, b) => (b.score ?? 0) - (a.score ?? 0)) const figures = (input.figures || []) .map(toFigureUnit) .filter((x): x is FigureUnit => x !== null) const footnotes = (input.footnotes || []) .map(toFootnoteUnit) .filter((x): x is FootnoteUnit => x !== null) const meta = { sources, figures, footnotes } return { ...meta, stats: deriveStats(meta) } }