feat(extraction): POST /compliance/extract-requirements-from-rag
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 34s
CI / test-python-backend-compliance (push) Successful in 31s
CI / test-python-document-crawler (push) Successful in 35s
CI / test-python-dsms-gateway (push) Successful in 17s

Sucht alle RAG-Kollektionen nach Prüfaspekten und legt automatisch
Anforderungen in der DB an. Kernfeatures:

- Durchsucht alle 6 RAG-Kollektionen parallel (bp_compliance_ce,
  bp_compliance_recht, bp_compliance_gesetze, bp_compliance_datenschutz,
  bp_dsfa_corpus, bp_legal_templates)
- Erkennt BSI Prüfaspekte (O.Purp_6) im Artikel-Feld und per Regex
- Dedupliziert nach (regulation_code, article) — safe to call many times
- Auto-erstellt Regulations-Stubs für unbekannte regulation_codes
- dry_run=true zeigt was erstellt würde ohne DB-Schreibzugriff
- Optionale Filter: collections, regulation_codes, search_queries
- 18 Tests (alle bestanden)
- Frontend: "Aus RAG extrahieren" Button auf /sdk/requirements

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-05 15:11:10 +01:00
parent f3ccfe5dcd
commit 3ed8300daf
4 changed files with 866 additions and 9 deletions

View File

@@ -404,6 +404,50 @@ export default function RequirementsPage() {
const [error, setError] = useState<string | null>(null)
const [showAddForm, setShowAddForm] = useState(false)
const [expandedId, setExpandedId] = useState<string | null>(null)
const [ragExtracting, setRagExtracting] = useState(false)
const [ragResult, setRagResult] = useState<{ created: number; skipped_duplicates: number; message: string } | null>(null)
const extractFromRAG = async () => {
setRagExtracting(true)
setRagResult(null)
try {
const res = await fetch('/api/sdk/v1/compliance/extract-requirements-from-rag', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ max_per_query: 20 }),
})
if (res.ok) {
const data = await res.json()
setRagResult({ created: data.created, skipped_duplicates: data.skipped_duplicates, message: data.message })
// Reload requirements list
const listRes = await fetch('/api/sdk/v1/compliance/requirements')
if (listRes.ok) {
const listData = await listRes.json()
const reqs = listData.requirements || listData
if (Array.isArray(reqs) && reqs.length > 0) {
const mapped = reqs.map((r: Record<string, unknown>) => ({
id: (r.requirement_id || r.id) as string,
regulation: (r.regulation_code || r.regulation || '') as string,
article: (r.article || '') as string,
title: (r.title || '') as string,
description: (r.description || '') as string,
criticality: ((r.criticality || r.priority || 'MEDIUM') as string).toUpperCase() as import('@/lib/sdk').RiskSeverity,
applicableModules: [] as string[],
status: 'NOT_STARTED' as import('@/lib/sdk').RequirementStatus,
controls: [] as string[],
}))
dispatch({ type: 'SET_STATE', payload: { requirements: mapped } })
}
}
} else {
setRagResult({ created: 0, skipped_duplicates: 0, message: 'RAG-Extraktion fehlgeschlagen' })
}
} catch {
setRagResult({ created: 0, skipped_duplicates: 0, message: 'RAG-Extraktion nicht erreichbar' })
} finally {
setRagExtracting(false)
}
}
// Fetch requirements from backend on mount
useEffect(() => {
@@ -626,17 +670,46 @@ export default function RequirementsPage() {
explanation={stepInfo.explanation}
tips={stepInfo.tips}
>
<button
onClick={() => setShowAddForm(true)}
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Anforderung hinzufuegen
</button>
<div className="flex items-center gap-2">
<button
onClick={extractFromRAG}
disabled={ragExtracting}
className="flex items-center gap-2 px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 disabled:opacity-60 transition-colors"
>
{ragExtracting ? (
<svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z" />
</svg>
) : (
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.347.347a3.5 3.5 0 01-4.95 0l-.347-.347z" />
</svg>
)}
Aus RAG extrahieren
</button>
<button
onClick={() => setShowAddForm(true)}
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Anforderung hinzufuegen
</button>
</div>
</StepHeader>
{/* RAG Extraction Result Banner */}
{ragResult && (
<div className={`flex items-center justify-between p-3 rounded-lg border ${ragResult.created > 0 ? 'bg-green-50 border-green-200' : 'bg-blue-50 border-blue-200'}`}>
<span className="text-sm">
{ragResult.created > 0 ? '✅' : ''} {ragResult.message}
</span>
<button onClick={() => setRagResult(null)} className="text-gray-400 hover:text-gray-600 ml-4">&times;</button>
</div>
)}
{/* Add Form */}
{showAddForm && (
<AddRequirementForm