feat(ai-sdk): legal-corpus structure endpoint + coverage page
Expose GET /sdk/v1/rag/legal-corpus, which scrolls the eur-lex legal corpus (filtered to a few hundred points regardless of total size) and aggregates each ingested act's composition: distinct articles, annexes, recitals and chunk count. Surface it as a new section on /sdk/coverage so the ingested corpus is no longer a black box — a developer SEES what each act actually contains, not only its name. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,7 @@ import Link from 'next/link'
|
||||
import {
|
||||
type UseCaseRow,
|
||||
type CorpusOverview,
|
||||
type LegalCorpus,
|
||||
licenseTierBadgeClass,
|
||||
commercialBadgeClass,
|
||||
groupUseCases,
|
||||
@@ -11,28 +12,46 @@ import {
|
||||
|
||||
const BACKEND_URL =
|
||||
process.env.COMPLIANCE_BACKEND_URL || 'http://backend-compliance:8002'
|
||||
// The legal-corpus structure comes from the Go SDK (it owns the vector store).
|
||||
const SDK_URL = process.env.SDK_URL || 'http://ai-compliance-sdk:8090'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
// Fetched from the SDK and isolated in its own try/catch so a vector-store
|
||||
// hiccup degrades to "no structure shown" instead of blanking the whole page.
|
||||
async function fetchLegalCorpus(): Promise<LegalCorpus | null> {
|
||||
try {
|
||||
const res = await fetch(`${SDK_URL}/sdk/v1/rag/legal-corpus`, {
|
||||
cache: 'no-store',
|
||||
})
|
||||
return res.ok ? await res.json() : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function getData(): Promise<{
|
||||
useCases: UseCaseRow[]
|
||||
corpus: CorpusOverview | null
|
||||
legalCorpus: LegalCorpus | null
|
||||
}> {
|
||||
try {
|
||||
const [ucRes, corpusRes] = await Promise.all([
|
||||
const [ucRes, corpusRes, legalCorpus] = await Promise.all([
|
||||
fetch(`${BACKEND_URL}/api/compliance/v1/controls/use-cases`, {
|
||||
cache: 'no-store',
|
||||
}),
|
||||
fetch(`${BACKEND_URL}/api/compliance/v1/controls/corpus`, {
|
||||
cache: 'no-store',
|
||||
}),
|
||||
fetchLegalCorpus(),
|
||||
])
|
||||
return {
|
||||
useCases: ucRes.ok ? await ucRes.json() : [],
|
||||
corpus: corpusRes.ok ? await corpusRes.json() : null,
|
||||
legalCorpus,
|
||||
}
|
||||
} catch {
|
||||
return { useCases: [], corpus: null }
|
||||
return { useCases: [], corpus: null, legalCorpus: null }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +65,7 @@ function Stat({ label, value }: { label: string; value: string | number }) {
|
||||
}
|
||||
|
||||
export default async function CoveragePage() {
|
||||
const { useCases, corpus } = await getData()
|
||||
const { useCases, corpus, legalCorpus } = await getData()
|
||||
const groups = groupUseCases(useCases)
|
||||
const totalRelevant = useCases.reduce((s, u) => s + u.atom_relevant, 0)
|
||||
const totalAtoms = useCases.reduce((s, u) => s + u.atom_total, 0)
|
||||
@@ -221,6 +240,67 @@ export default async function CoveragePage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{legalCorpus?.regulations?.length ? (
|
||||
<section className="space-y-2">
|
||||
<h2 className="text-lg font-semibold text-gray-900">
|
||||
Ingestierter Rechtskorpus – Struktur ({legalCorpus.totals.regulations}{' '}
|
||||
Rechtsakte)
|
||||
</h2>
|
||||
<p className="text-xs text-gray-500">
|
||||
Woraus jeder ingestierte eur-lex-Rechtsakt tatsächlich besteht:
|
||||
Artikel (§), Anhänge, Erwägungsgründe und retrievbare Chunks — direkt
|
||||
aus dem Vektorspeicher, damit kein Black-Box-Korpus entsteht.
|
||||
</p>
|
||||
<div className="overflow-auto rounded-lg border border-gray-200">
|
||||
<table className="min-w-full divide-y divide-gray-200 text-sm">
|
||||
<thead className="bg-gray-50 text-left text-xs uppercase text-gray-500">
|
||||
<tr>
|
||||
<th className="px-4 py-2">Rechtsakt</th>
|
||||
<th className="px-4 py-2 text-right">Artikel (§)</th>
|
||||
<th className="px-4 py-2 text-right">Anhänge</th>
|
||||
<th className="px-4 py-2 text-right">Erwägungsgründe</th>
|
||||
<th className="px-4 py-2 text-right">Chunks</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100 bg-white">
|
||||
{legalCorpus.regulations.map((r) => (
|
||||
<tr key={r.regulation_short}>
|
||||
<td className="px-4 py-2 text-gray-900">
|
||||
<span className="font-medium">{r.regulation_short}</span>
|
||||
{r.regulation_name !== r.regulation_short ? (
|
||||
<span className="ml-2 text-xs text-gray-500">
|
||||
{r.regulation_name}
|
||||
</span>
|
||||
) : null}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-right font-semibold">
|
||||
{r.articles.toLocaleString('de-DE')}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-right">
|
||||
{r.annexes > 0 ? (
|
||||
r.annexes.toLocaleString('de-DE')
|
||||
) : (
|
||||
<span className="text-gray-300">—</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-right text-gray-500">
|
||||
{r.recitals > 0 ? (
|
||||
r.recitals.toLocaleString('de-DE')
|
||||
) : (
|
||||
<span className="text-gray-300">—</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-right text-gray-500">
|
||||
{r.chunks.toLocaleString('de-DE')}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{corpus?.license_catalog?.length ? (
|
||||
<section className="space-y-2">
|
||||
<h2 className="text-lg font-semibold text-gray-900">
|
||||
|
||||
Reference in New Issue
Block a user