feat(advisor): Evidence Workspace — structured panes, markdown, sources as knowledge units
Rebuilds the Compliance Advisor floating widget from a plain chat into an Evidence
Workspace: pinned last question, markdown-rendered answer (clean prose), and separate
panes for Sources (hierarchical Knowledge Units), Figures (C8, conditional) and
Footnotes (C-FN), plus a stats bar (Quellen/Regelwerke/Diagramme/Fußnoten). Scrollable
turn history; stays a floating icon on every SDK page.
Architecture (user direction): the frontend renders ONLY structured evidence and NEVER
parses the answer text. The proxy now returns a JSON AdvisorEvidenceMeta line followed
by the streamed markdown answer; advisor-rag exposes structured results; an adapter maps
RAG/compiler output to the frontend envelope. Figures/footnotes wire in once the
RAG-ingestion contract lands (requested on the board) — figures pane is conditional.
- lib/sdk/advisor/{evidence,evidence-adapter}.ts (+ adapter test, 7 cases)
- components/sdk/advisor/* panes + in-house safe Markdown (no new dep, no dangerouslySetInnerHTML) + test
- useAdvisorStream (meta-line parse + streamed answer) + useAdvisorEmail (escaped)
- proxy: evidence-meta-v1 envelope + clean-prose prompt (no inline citations)
- tsc clean, 11 vitest pass, check-loc 0. ESLint not installed in this node_modules -> CI lints on push.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { adaptEvidence } from '../advisor/evidence-adapter'
|
||||
import type { SdkRagResult } from '../agents/advisor-rag'
|
||||
|
||||
describe('adaptEvidence', () => {
|
||||
it('maps a structured RAG result to a hierarchical Knowledge Unit', () => {
|
||||
const results: SdkRagResult[] = [
|
||||
{
|
||||
text: 'Der Verantwortliche fuehrt ein Verzeichnis ...',
|
||||
regulation_code: 'DSGVO',
|
||||
regulation_short: 'DSGVO',
|
||||
article_label: 'Art. 30 DSGVO',
|
||||
article: 'Art. 30',
|
||||
paragraph: 'Abs. 1',
|
||||
source_url: 'https://example.test/dsgvo-30',
|
||||
score: 0.9,
|
||||
},
|
||||
]
|
||||
const { sources, stats } = adaptEvidence({ results })
|
||||
expect(sources).toHaveLength(1)
|
||||
expect(sources[0].label).toBe('Art. 30 DSGVO')
|
||||
expect(sources[0].section).toBe('Art. 30')
|
||||
expect(sources[0].paragraph).toBe('Abs. 1')
|
||||
expect(sources[0].open?.originalUrl).toBe('https://example.test/dsgvo-30')
|
||||
expect(sources[0].snippet).toContain('Verzeichnis')
|
||||
expect(stats.sources).toBe(1)
|
||||
expect(stats.regulations).toBe(1)
|
||||
})
|
||||
|
||||
it('dedupes the same citation and keeps the highest score', () => {
|
||||
const base: SdkRagResult = {
|
||||
text: 'x',
|
||||
regulation_code: 'CRA',
|
||||
regulation_short: 'CRA',
|
||||
article_label: 'Annex I',
|
||||
article: 'Annex I',
|
||||
}
|
||||
const { sources } = adaptEvidence({
|
||||
results: [
|
||||
{ ...base, score: 0.4 },
|
||||
{ ...base, score: 0.8 },
|
||||
],
|
||||
})
|
||||
expect(sources).toHaveLength(1)
|
||||
expect(sources[0].score).toBe(0.8)
|
||||
})
|
||||
|
||||
it('counts distinct regulations in stats', () => {
|
||||
const { stats } = adaptEvidence({
|
||||
results: [
|
||||
{ text: 'a', regulation_code: 'DSGVO', article_label: 'Art. 5' },
|
||||
{ text: 'b', regulation_code: 'DSGVO', article_label: 'Art. 6' },
|
||||
{ text: 'c', regulation_code: 'BDSG', article_label: '§ 38' },
|
||||
],
|
||||
})
|
||||
expect(stats.sources).toBe(3)
|
||||
expect(stats.regulations).toBe(2)
|
||||
})
|
||||
|
||||
it('labels recitals as Erwaegungsgrund', () => {
|
||||
const { sources } = adaptEvidence({
|
||||
results: [{ text: 'r', regulation_code: 'DSGVO', is_recital: true, article: '47' }],
|
||||
})
|
||||
expect(sources[0].section).toBe('Erwägungsgrund 47')
|
||||
})
|
||||
|
||||
it('maps figures (C8) to figure units and counts them', () => {
|
||||
const { figures, stats } = adaptEvidence({
|
||||
results: [],
|
||||
figures: [
|
||||
{
|
||||
figure_id: 'fig-pdca',
|
||||
label: 'Abbildung 3',
|
||||
caption: 'PDCA-Zyklus',
|
||||
regulation_short: 'EDPB WP248',
|
||||
vision_summary: 'Kreislauf Plan-Do-Check-Act',
|
||||
image_url: 'https://example.test/abb3.png',
|
||||
},
|
||||
],
|
||||
})
|
||||
expect(figures).toHaveLength(1)
|
||||
expect(figures[0].label).toBe('Abbildung 3')
|
||||
expect(figures[0].caption).toBe('PDCA-Zyklus')
|
||||
expect(figures[0].imageUrl).toBe('https://example.test/abb3.png')
|
||||
expect(stats.figures).toBe(1)
|
||||
})
|
||||
|
||||
it('maps footnotes (C-FN) and counts them', () => {
|
||||
const { footnotes, stats } = adaptEvidence({
|
||||
results: [],
|
||||
footnotes: [{ number: 17, regulation_short: 'EDPB WP248', section: 'Kapitel III.B', text: 'siehe ...' }],
|
||||
})
|
||||
expect(footnotes).toHaveLength(1)
|
||||
expect(footnotes[0].ref).toBe('Fußnote 17')
|
||||
expect(stats.footnotes).toBe(1)
|
||||
})
|
||||
|
||||
it('returns empty evidence for empty input', () => {
|
||||
const meta = adaptEvidence({})
|
||||
expect(meta.sources).toEqual([])
|
||||
expect(meta.figures).toEqual([])
|
||||
expect(meta.footnotes).toEqual([])
|
||||
expect(meta.stats).toEqual({ sources: 0, regulations: 0, figures: 0, footnotes: 0 })
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,145 @@
|
||||
// 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<string, KnowledgeUnit>()
|
||||
;(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) }
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
// Structured evidence contract for the Compliance Advisor "Evidence Workspace".
|
||||
//
|
||||
// HARD RULE (architecture): the frontend renders ONLY these structured fields and
|
||||
// NEVER parses the answer text. All structure (sources, figures, footnotes) is owned
|
||||
// by the SDK/compiler (C-stages) and surfaced as data. The proxy is the adapter that
|
||||
// fills this envelope from RAG/compiler output. See memory: advisor-evidence-workspace-no-parse.
|
||||
|
||||
/** A regulation / document reference (CRA, EDPB WP248, MaschinenVO, ...). */
|
||||
export interface RegulationRef {
|
||||
code: string // canonical id, e.g. "cra", "edpb_wp248", "maschinenvo"
|
||||
name?: string // full name
|
||||
short?: string // short label shown in the card header
|
||||
}
|
||||
|
||||
/** Openable targets for an evidence item — present only when the SDK can resolve them. */
|
||||
export interface OpenTargets {
|
||||
originalUrl?: string // original text / source_url
|
||||
chunkId?: string // retrieved chunk
|
||||
footnoteId?: string // C-FN
|
||||
figureId?: string // C8
|
||||
}
|
||||
|
||||
/**
|
||||
* A retrieved source as a hierarchical Knowledge Unit, mirroring the compiler:
|
||||
* Regelwerk -> Section (C1/C2) -> Paragraph -> Footnote (C-FN).
|
||||
* Rendered as a card, not a text-list line. E.g. "EDPB WP248 / Kapitel III.B / Fußnote 17".
|
||||
*/
|
||||
export interface KnowledgeUnit {
|
||||
id: string
|
||||
regulation: RegulationRef
|
||||
section?: string // "Annex I" / "Kapitel III.B" / "Anhang III"
|
||||
subsection?: string // "Abschnitt 2.3"
|
||||
paragraph?: string // Absatz / paragraph
|
||||
footnoteRef?: string // "Fußnote 17" when this unit IS a footnote-backed source
|
||||
label?: string // pre-formatted citation fallback, e.g. "BDSG § 38 Abs. 1"
|
||||
score?: number // retrieval score (optional)
|
||||
snippet?: string // short passage preview (optional) — lets the user peek the cited text
|
||||
open?: OpenTargets
|
||||
}
|
||||
|
||||
/** A figure (C8) as a Knowledge Unit — never a bare image. Only present when figures exist. */
|
||||
export interface FigureUnit {
|
||||
id: string // figure_id
|
||||
label: string // "Abbildung 3"
|
||||
caption?: string // "PDCA-Zyklus"
|
||||
topic?: string
|
||||
source: RegulationRef // "EDPB ..."
|
||||
section?: string
|
||||
visionSummary?: string // vision/LLM description of the figure
|
||||
imageUrl?: string // Playwright PNG; undefined until the RAG-ingestion contract delivers it
|
||||
}
|
||||
|
||||
/** A footnote (C-FN) as a first-class evidence item. */
|
||||
export interface FootnoteUnit {
|
||||
id: string
|
||||
ref: string // "Fußnote 17"
|
||||
source: RegulationRef
|
||||
section?: string
|
||||
text?: string
|
||||
}
|
||||
|
||||
/** Counts for the stats bar above the answer ("Diese Antwort basiert auf N Quellen"). */
|
||||
export interface AdvisorStats {
|
||||
sources: number
|
||||
regulations: number // distinct Regelwerke
|
||||
figures: number
|
||||
footnotes: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Meta sent by the proxy FIRST (one JSON line), then the answer streams as tokens.
|
||||
* RAG runs before the LLM, so all evidence is known up front and the panes render
|
||||
* immediately while the answer streams in.
|
||||
*/
|
||||
export interface AdvisorEvidenceMeta {
|
||||
stats: AdvisorStats
|
||||
sources: KnowledgeUnit[]
|
||||
figures: FigureUnit[]
|
||||
footnotes: FootnoteUnit[]
|
||||
relatedDocs?: KnowledgeUnit[]
|
||||
}
|
||||
|
||||
/** The full evidence a single answer turn holds (meta + the streamed answer markdown). */
|
||||
export interface AdvisorEvidence extends AdvisorEvidenceMeta {
|
||||
answer: string // markdown prose, NO inline citations (sources live in the pane)
|
||||
}
|
||||
|
||||
export function emptyStats(): AdvisorStats {
|
||||
return { sources: 0, regulations: 0, figures: 0, footnotes: 0 }
|
||||
}
|
||||
|
||||
/** Pure derivation of the stats bar from the evidence items (no parsing of answer text). */
|
||||
export function deriveStats(
|
||||
e: Pick<AdvisorEvidenceMeta, 'sources' | 'figures' | 'footnotes'>,
|
||||
): AdvisorStats {
|
||||
const regulations = new Set(e.sources.map((s) => s.regulation.code))
|
||||
return {
|
||||
sources: e.sources.length,
|
||||
regulations: regulations.size,
|
||||
figures: e.figures.length,
|
||||
footnotes: e.footnotes.length,
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,9 @@
|
||||
* Advisor bleibt damit collection-agnostisch (Vertrag: Compiler -> Collections -> Retriever
|
||||
* -> Advisor); die fruehere Multi-Collection-Logik liegt jetzt im Retriever.
|
||||
*
|
||||
* Fehler werden geschluckt (graceful: Antwort ohne RAG-Kontext).
|
||||
* Fundstellen via article_label sind live ab dem Prod-Re-Ingest 2026-06.
|
||||
* `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 =
|
||||
@@ -18,7 +19,7 @@ const DEFAULT_USER = '00000000-0000-0000-0000-000000000001'
|
||||
const DEFAULT_TENANT =
|
||||
process.env.DEFAULT_TENANT_ID || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e'
|
||||
|
||||
interface SdkRagResult {
|
||||
export interface SdkRagResult {
|
||||
text?: string
|
||||
regulation_code?: string
|
||||
regulation_name?: string
|
||||
@@ -34,20 +35,27 @@ interface SdkRagResult {
|
||||
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}. */
|
||||
/** 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
|
||||
// (bzw. alt-ingestierte Chunks ohne Legal-Metadaten). Siehe rag_reingest_spec.md §2/§7.
|
||||
// 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]
|
||||
@@ -59,15 +67,16 @@ export function mapSdkResults(results: SdkRagResult[] | undefined): ScoredPassag
|
||||
.filter((p) => p.content)
|
||||
}
|
||||
|
||||
/**
|
||||
* Authority Router: EIN collection-agnostischer Aufruf an die ai-sdk (`/sdk/v1/rag/retrieve`).
|
||||
* Der Router waehlt die Collections (Broad-Authority-Base + KB-2026.1-Slice bei in-scope),
|
||||
* merged + authority-ranked sie und liefert die Top-Passagen. Der Advisor weiss damit nichts
|
||||
* mehr ueber einzelne Collections — die fruehere Multi-Collection-Logik liegt jetzt im Retriever.
|
||||
* Fehler werden geschluckt (graceful: Antwort ohne RAG-Kontext).
|
||||
*/
|
||||
export async function queryAdvisorRAG(query: string): Promise<string> {
|
||||
let passages: ScoredPassage[] = []
|
||||
/** 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<SdkRagResponse> {
|
||||
try {
|
||||
const res = await fetch(`${SDK_URL}/sdk/v1/rag/retrieve`, {
|
||||
method: 'POST',
|
||||
@@ -79,16 +88,37 @@ export async function queryAdvisorRAG(query: string): Promise<string> {
|
||||
body: JSON.stringify({ query, top_k: 8 }),
|
||||
signal: AbortSignal.timeout(15000),
|
||||
})
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
passages = mapSdkResults(data.results)
|
||||
}
|
||||
if (res.ok) return ((await res.json()) as SdkRagResponse) || {}
|
||||
} catch {
|
||||
// graceful: keine Verbindung -> Antwort ohne RAG-Kontext
|
||||
}
|
||||
// Der Router liefert bereits authority-geordnete Top-K; Reihenfolge bewahren.
|
||||
if (passages.length === 0) return ''
|
||||
return passages
|
||||
.map((r, i) => `[Quelle ${i + 1}: ${r.source}]\n${r.content}`)
|
||||
.join('\n\n---\n\n')
|
||||
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<AdvisorEvidenceRaw> {
|
||||
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<string> {
|
||||
return (await retrieveAdvisorEvidence(query)).contextText
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user