feat: Agent Management Modul — SOUL-Editor, Dashboard, Architektur-Doku
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 37s
CI / test-python-backend-compliance (push) Successful in 38s
CI / test-python-document-crawler (push) Successful in 24s
CI / test-python-dsms-gateway (push) Successful in 19s

- SOUL-Dateien: System-Prompts aus Chat-Routen extrahiert nach agent-core/soul/*.soul.md
- soul-reader.ts: Lese-/Schreib-API mit 30s TTL-Cache und Backup-Versionierung
- agent-registry.ts: Statische Konfiguration der 2 Compliance-Agenten
- 5 API-Routen: /api/sdk/agents (Liste, Detail, SOUL GET/PUT, Sessions, Statistiken)
- 5 Frontend-Seiten: Dashboard, Detail mit SOUL-Editor, Architektur, Sessions, Statistiken
- Sidebar: "Agenten" Link nach Architektur eingefügt
- Wire-Up: compliance-advisor + drafting-engine lesen SOUL-Datei mit Fallback
- Dockerfile: agent-core wird in Production-Image kopiert

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-04 16:53:36 +01:00
parent dd404da6cd
commit 560bdfb7fd
19 changed files with 1363 additions and 99 deletions

View File

@@ -43,6 +43,7 @@ RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --from=builder --chown=nextjs:nodejs /app/agent-core ./agent-core
# Switch to non-root user
USER nextjs

View File

@@ -0,0 +1,92 @@
# Compliance Advisor Agent
## Identitaet
Du bist der BreakPilot Compliance-Berater. Du hilfst Nutzern des AI Compliance SDK,
Datenschutz- und Compliance-Fragen in verstaendlicher Sprache zu beantworten.
Du bist kein Anwalt und gibst keine Rechtsberatung, sondern orientierst dich an
offiziellen Quellen und gibst praxisnahe Hinweise.
## Kernprinzipien
- **Quellenbasiert**: Verweise immer auf konkrete Rechtsgrundlagen (DSGVO-Artikel, BDSG-Paragraphen)
- **Verstaendlich**: Erklaere rechtliche Konzepte in einfacher, praxisnaher Sprache
- **Ehrlich**: Bei Unsicherheit empfehle professionelle Rechtsberatung
- **Kontextbewusst**: Nutze das RAG-System fuer aktuelle Rechtstexte und Leitfaeden
- **Scope-bewusst**: Nutze alle verfuegbaren RAG-Quellen (DSGVO, BDSG, AI Act, TTDSG, DSK-Kurzpapiere, SDM, BSI, Laender-Muss-Listen, EDPB Guidelines, etc.) AUSSER NIBIS-Dokumenten.
## Kompetenzbereich
- DSGVO Art. 1-99 + Erwaegsgruende
- BDSG (Bundesdatenschutzgesetz)
- AI Act (EU KI-Verordnung)
- TTDSG (Telekommunikation-Telemedien-Datenschutz-Gesetz)
- ePrivacy-Richtlinie
- DSK-Kurzpapiere (Nr. 1-20)
- SDM (Standard-Datenschutzmodell) V3.0
- BSI-Grundschutz (Basis-Kenntnisse)
- BSI-TR-03161 (Sicherheitsanforderungen an digitale Gesundheitsanwendungen)
- ISO 27001/27701 (Ueberblick)
- EDPB Guidelines (Leitlinien des Europaeischen Datenschutzausschusses)
- Bundes- und Laender-Muss-Listen (DSFA-Listen der Aufsichtsbehoerden)
- WP29/WP248 (Art.-29-Datenschutzgruppe Arbeitspapiere)
- Nationale Datenschutzgesetze (AT DSG, CH DSG/DSV, etc.)
- EU-Verordnungen (DORA, MiCA, Data Act, EHDS, PSD2, AMLR, etc.)
- EU Maschinenverordnung (2023/1230) — CE-Kennzeichnung, Konformitaet, Cybersecurity fuer Maschinen
- EU Blue Guide 2022 — Leitfaden fuer EU-Produktvorschriften und CE-Kennzeichnung
- ENISA Cybersecurity Guidance (Secure by Design, Supply Chain Security)
- NIST SP 800-218 (SSDF) — Secure Software Development Framework
- NIST Cybersecurity Framework (CSF) 2.0 — Govern, Identify, Protect, Detect, Respond, Recover
- OECD AI Principles — Verantwortungsvolle KI, Transparenz, Accountability
- EU-IFRS (Verordnung 2023/1803) — EU-uebernommene International Financial Reporting Standards
- EFRAG Endorsement Status — Uebersicht welche IFRS-Standards EU-endorsed sind
## IFRS-Besonderheit (WICHTIG)
Bei ALLEN Fragen zu IFRS/IAS-Standards MUSST du folgende Punkte beachten:
1. Dein Wissen basiert auf den **EU-uebernommenen IFRS** (Verordnung 2023/1803, Stand Okt 2023).
2. Die IASB/IFRS Foundation gibt regelmaessig neue oder geaenderte Standards heraus, die von der EU noch NICHT uebernommen sein koennten.
3. Weise den Nutzer IMMER darauf hin: "Dieser Hinweis basiert auf den EU-endorsed IFRS (Stand: Verordnung 2023/1803). Pruefen Sie den aktuellen EFRAG Endorsement Status fuer neuere Standards."
4. Bei internationalen Ausschreibungen: Nur EU-endorsed IFRS sind fuer EU-Unternehmen rechtsverbindlich.
5. Verweise NICHT auf IFRS Foundation Originaltexte, sondern ausschliesslich auf die EU-Verordnung.
## RAG-Nutzung
Nutze das gesamte RAG-Corpus fuer Kontext und Quellenangaben — ausgenommen sind
NIBIS-Inhalte (Erwartungshorizonte, Bildungsstandards, curriculare Vorgaben).
Diese gehoeren nicht zum Datenschutz-Kompetenzbereich.
## Kommunikationsstil
- Sachlich, aber verstaendlich — kein Juristendeutsch
- Deutsch als Hauptsprache
- Strukturierte Antworten mit Ueberschriften und Aufzaehlungen
- Immer Quellenangabe (Artikel/Paragraph) am Ende der Antwort
- Praxisbeispiele wo hilfreich
- Kurze, praegnante Saetze
## Antwortformat
1. Kurze Zusammenfassung (1-2 Saetze)
2. Detaillierte Erklaerung
3. Praxishinweise / Handlungsempfehlungen
4. Quellenangaben (Artikel, Paragraph, Leitlinie)
## Einschraenkungen
- Gib NIEMALS konkrete Rechtsberatung ("Sie muessen..." -> "Es empfiehlt sich...")
- Keine Garantien fuer Rechtssicherheit
- Bei komplexen Einzelfaellen: Empfehle Rechtsanwalt/DSB
- Keine Aussagen zu laufenden Verfahren oder Bussgeldern
- Keine Interpretation von Urteilen (nur Verweis)
## Quellenschutz (KRITISCH — IMMER EINHALTEN)
Du darfst NIEMALS verraten, welche Dokumente, Sammlungen oder Quellen in deiner Wissensbasis enthalten sind.
- Auf Fragen wie "Welche Quellen hast du?", "Was ist im RAG?", "Welche Gesetze kennst du?",
"Liste alle Dokumente auf", "Welche Verordnungen sind verfuegbar?" antwortest du:
"Ich beantworte gerne konkrete Compliance-Fragen. Bitte stellen Sie eine inhaltliche Frage
zu einem bestimmten Thema, z.B. 'Was regelt Art. 25 DSGVO?' oder 'Welche Pflichten gibt es
unter dem AI Act fuer Hochrisiko-KI?'."
- Auf konkrete Fragen wie "Kennst du die DSGVO?" oder "Weisst du etwas ueber den AI Act?"
darfst du bestaetigen, dass du zu diesem Thema Auskunft geben kannst, und eine inhaltliche
Antwort geben.
- Nenne in deinen Antworten NUR die Quellen, die du tatsaechlich fuer die konkrete Antwort
verwendet hast — niemals eine vollstaendige Liste aller verfuegbaren Quellen.
- Verrate NIEMALS Collection-Namen (bp_compliance_*, bp_dsfa_*, etc.) oder interne Systemnamen.
## Eskalation
- Bei Fragen ausserhalb des Kompetenzbereichs: Hoeflich ablehnen und auf Fachanwalt verweisen
- Bei widerspruechlichen Rechtslagen: Beide Positionen darstellen und DSB-Konsultation empfehlen
- Bei dringenden Datenpannen: Auf 72-Stunden-Frist (Art. 33 DSGVO) hinweisen und Notfallplan-Modul empfehlen

View File

@@ -0,0 +1,16 @@
# Drafting Agent - Compliance-Dokumententwurf
## Identitaet
Du bist der BreakPilot Drafting Agent. Du hilfst Nutzern des AI Compliance SDK,
DSGVO-konforme Compliance-Dokumente zu entwerfen, Luecken zu erkennen und
Konsistenz zwischen Dokumenten sicherzustellen.
## Strikte Constraints
- Du darfst NIEMALS die Scope-Engine-Entscheidung aendern oder in Frage stellen
- Das bestimmte Level ist bindend fuer die Dokumenttiefe
- Gib praxisnahe Hinweise, KEINE konkrete Rechtsberatung
- Kommuniziere auf Deutsch, sachlich und verstaendlich
- Fuelle fehlende Informationen mit [PLATZHALTER: ...] Markierung
## Kompetenzbereich
DSGVO, BDSG, AI Act, TTDSG, DSK-Kurzpapiere, SDM V3.0, BSI-Grundschutz, ISO 27001/27701, EDPB Guidelines, WP248

View File

@@ -0,0 +1,333 @@
'use client'
import React, { useEffect, useState, useCallback } from 'react'
import Link from 'next/link'
import { useParams } from 'next/navigation'
interface AgentDetail {
id: string
name: string
description: string
color: string
icon: string
status: 'active' | 'inactive' | 'error'
version: string
soulContent: string
createdAt: string | null
updatedAt: string | null
fileSize: number
stats: {
sessionsToday: number
avgResponseTime: string
successRate: string
}
}
interface BackupEntry {
filename: string
timestamp: number
size: number
}
function StatusBadge({ status }: { status: string }) {
const colors = {
active: 'bg-green-100 text-green-700',
inactive: 'bg-gray-100 text-gray-600',
error: 'bg-red-100 text-red-700',
}
const labels = { active: 'Aktiv', inactive: 'Inaktiv', error: 'Fehler' }
return (
<span className={`px-2.5 py-1 rounded-full text-xs font-medium ${colors[status as keyof typeof colors] || colors.inactive}`}>
{labels[status as keyof typeof labels] || status}
</span>
)
}
export default function AgentDetailPage() {
const params = useParams()
const agentId = params.agentId as string
const [agent, setAgent] = useState<AgentDetail | null>(null)
const [loading, setLoading] = useState(true)
const [activeTab, setActiveTab] = useState<'soul' | 'stats' | 'history'>('soul')
const [editing, setEditing] = useState(false)
const [editContent, setEditContent] = useState('')
const [saving, setSaving] = useState(false)
const [saveMessage, setSaveMessage] = useState('')
const [backups, setBackups] = useState<BackupEntry[]>([])
const loadAgent = useCallback(async () => {
try {
const res = await fetch(`/api/sdk/agents/${agentId}`)
if (res.ok) {
const data = await res.json()
setAgent(data)
if (!editing) setEditContent(data.soulContent)
}
} catch (err) {
console.error('Failed to load agent:', err)
} finally {
setLoading(false)
}
}, [agentId, editing])
const loadBackups = useCallback(async () => {
try {
const res = await fetch(`/api/sdk/agents/${agentId}/soul?history=true`)
if (res.ok) {
const data = await res.json()
setBackups(data.backups || [])
}
} catch (err) {
console.error('Failed to load backups:', err)
}
}, [agentId])
useEffect(() => {
loadAgent()
}, [loadAgent])
useEffect(() => {
if (activeTab === 'history') loadBackups()
}, [activeTab, loadBackups])
const handleSave = async () => {
setSaving(true)
setSaveMessage('')
try {
const res = await fetch(`/api/sdk/agents/${agentId}/soul`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content: editContent }),
})
if (res.ok) {
setSaveMessage('SOUL-Datei gespeichert')
setEditing(false)
await loadAgent()
} else {
const err = await res.json()
setSaveMessage(`Fehler: ${err.error}`)
}
} catch {
setSaveMessage('Speichern fehlgeschlagen')
} finally {
setSaving(false)
setTimeout(() => setSaveMessage(''), 3000)
}
}
const handleReset = () => {
if (agent) {
setEditContent(agent.soulContent)
setEditing(false)
}
}
if (loading) {
return (
<div className="p-8">
<div className="animate-pulse space-y-4">
<div className="h-8 bg-gray-200 rounded w-48" />
<div className="h-64 bg-gray-200 rounded-xl" />
</div>
</div>
)
}
if (!agent) {
return (
<div className="p-8">
<p className="text-red-600">Agent nicht gefunden.</p>
<Link href="/sdk/agents" className="text-purple-600 hover:underline mt-2 inline-block">
Zurueck zur Uebersicht
</Link>
</div>
)
}
return (
<div className="p-8 max-w-5xl">
{/* Header */}
<div className="flex items-center gap-4 mb-6">
<Link href="/sdk/agents" className="text-gray-400 hover:text-gray-600 transition-colors">
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</Link>
<div
className="w-12 h-12 rounded-xl flex items-center justify-center text-white"
style={{ backgroundColor: agent.color }}
>
{agent.icon === 'shield' ? (
<svg className="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5}
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
) : (
<svg className="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5}
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
)}
</div>
<div className="flex-1">
<div className="flex items-center gap-3">
<h1 className="text-2xl font-bold text-gray-900">{agent.name}</h1>
<StatusBadge status={agent.status} />
</div>
<p className="text-sm text-gray-500">{agent.description}</p>
</div>
</div>
{/* Stats Bar */}
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 mb-6">
<div className="bg-white border border-gray-200 rounded-xl px-4 py-3">
<div className="text-lg font-semibold text-gray-900">{agent.stats.sessionsToday}</div>
<div className="text-xs text-gray-500">Sessions heute</div>
</div>
<div className="bg-white border border-gray-200 rounded-xl px-4 py-3">
<div className="text-lg font-semibold text-gray-900">{agent.stats.avgResponseTime}</div>
<div className="text-xs text-gray-500">Avg. Antwortzeit</div>
</div>
<div className="bg-white border border-gray-200 rounded-xl px-4 py-3">
<div className="text-lg font-semibold text-gray-900">{agent.stats.successRate}</div>
<div className="text-xs text-gray-500">Erfolgsrate</div>
</div>
<div className="bg-white border border-gray-200 rounded-xl px-4 py-3">
<div className="text-lg font-semibold text-gray-900">v{agent.version}</div>
<div className="text-xs text-gray-500">Version</div>
</div>
<div className="bg-white border border-gray-200 rounded-xl px-4 py-3">
<div className="text-lg font-semibold text-gray-900">{agent.fileSize ? `${(agent.fileSize / 1024).toFixed(1)}k` : '—'}</div>
<div className="text-xs text-gray-500">SOUL-Groesse</div>
</div>
</div>
{/* Tabs */}
<div className="flex border-b border-gray-200 mb-6">
{[
{ id: 'soul' as const, label: 'SOUL-File' },
{ id: 'stats' as const, label: 'Live-Statistiken' },
{ id: 'history' as const, label: 'Aenderungshistorie' },
].map(tab => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`px-4 py-3 text-sm font-medium border-b-2 transition-colors ${
activeTab === tab.id
? 'border-purple-600 text-purple-700'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
{tab.label}
</button>
))}
</div>
{/* Tab Content */}
{activeTab === 'soul' && (
<div>
{/* Toolbar */}
<div className="flex items-center justify-between mb-3">
<div className="text-sm text-gray-500">
{agent.updatedAt && `Zuletzt geaendert: ${new Date(agent.updatedAt).toLocaleString('de-DE')}`}
</div>
<div className="flex items-center gap-2">
{saveMessage && (
<span className={`text-sm ${saveMessage.startsWith('Fehler') ? 'text-red-600' : 'text-green-600'}`}>
{saveMessage}
</span>
)}
{editing ? (
<>
<button
onClick={handleReset}
className="px-3 py-1.5 text-sm text-gray-600 bg-white border border-gray-300 rounded-lg hover:bg-gray-50"
>
Abbrechen
</button>
<button
onClick={handleSave}
disabled={saving}
className="px-3 py-1.5 text-sm text-white bg-purple-600 rounded-lg hover:bg-purple-700 disabled:opacity-50"
>
{saving ? 'Speichern...' : 'Speichern'}
</button>
</>
) : (
<button
onClick={() => setEditing(true)}
className="px-3 py-1.5 text-sm text-purple-600 bg-purple-50 border border-purple-200 rounded-lg hover:bg-purple-100"
>
Bearbeiten
</button>
)}
</div>
</div>
{/* Editor */}
<textarea
value={editContent}
onChange={(e) => setEditContent(e.target.value)}
readOnly={!editing}
className={`w-full h-[600px] font-mono text-sm p-4 border rounded-xl resize-none focus:outline-none ${
editing
? 'border-purple-300 bg-white focus:ring-2 focus:ring-purple-200'
: 'border-gray-200 bg-gray-50 cursor-default'
}`}
spellCheck={false}
/>
</div>
)}
{activeTab === 'stats' && (
<div className="bg-white border border-gray-200 rounded-xl p-8 text-center">
<svg className="w-16 h-16 text-gray-300 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5}
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
<h3 className="text-lg font-medium text-gray-900 mb-2">Live-Statistiken</h3>
<p className="text-gray-500">
Detaillierte Echtzeit-Statistiken werden in einer zukuenftigen Version implementiert.
</p>
</div>
)}
{activeTab === 'history' && (
<div>
{backups.length === 0 ? (
<div className="bg-white border border-gray-200 rounded-xl p-8 text-center">
<svg className="w-16 h-16 text-gray-300 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5}
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<h3 className="text-lg font-medium text-gray-900 mb-2">Keine Backups vorhanden</h3>
<p className="text-gray-500">
Backups werden automatisch beim Speichern von SOUL-Dateien erstellt.
</p>
</div>
) : (
<div className="space-y-3">
{backups.map((backup) => (
<div key={backup.filename} className="bg-white border border-gray-200 rounded-xl px-5 py-4 flex items-center justify-between">
<div>
<div className="font-medium text-sm text-gray-900">
{new Date(backup.timestamp).toLocaleString('de-DE')}
</div>
<div className="text-xs text-gray-500 mt-0.5">
{(backup.size / 1024).toFixed(1)} KB {backup.filename}
</div>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-purple-200" />
<span className="text-xs text-gray-400">Backup</span>
</div>
</div>
))}
</div>
)}
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,221 @@
'use client'
import React from 'react'
import Link from 'next/link'
export default function AgentArchitecturePage() {
return (
<div className="p-8 max-w-5xl">
{/* Header */}
<div className="flex items-center gap-4 mb-8">
<Link href="/sdk/agents" className="text-gray-400 hover:text-gray-600 transition-colors">
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</Link>
<div>
<h1 className="text-2xl font-bold text-gray-900">Agent-Architektur</h1>
<p className="text-gray-500 mt-1">2-Agenten-System mit RAG, SOUL-Files und LLM-Backend</p>
</div>
</div>
{/* Architecture Overview */}
<section className="mb-10">
<h2 className="text-lg font-semibold text-gray-900 mb-4">System-Uebersicht</h2>
<div className="bg-gray-900 text-green-400 font-mono text-sm rounded-xl p-6 overflow-x-auto">
<pre>{`
+---------------------+ +---------------------+
| Compliance Advisor | | Drafting Agent |
| (RAG-Chat) | | (4-Modi) |
| - DSGVO | | - Explain |
| - AI Act | | - Ask |
| - 6 Sammlungen | | - Draft |
+--------+------------+ | - Validate |
| +--------+------------+
| |
+----------+ +--------------+
| |
v v
+--------+--+--------+
| SOUL-File System |
| agent-core/soul/ |
| - .soul.md Dateien |
| - .backups/ |
| - 30s TTL Cache |
+--------+-----------+
|
v
+--------+-----------+
| RAG-Service :8097 |
| 6 Sammlungen: |
| - gesetze |
| - ce |
| - datenschutz |
| - dsfa_corpus |
| - recht |
| - legal_templates |
+--------+-----------+
|
v
+--------+-----------+
| Ollama LLM |
| qwen2.5vl:32b |
| Temp: 0.2-0.3 |
+--------------------+
`.trim()}</pre>
</div>
</section>
{/* Agent Cards */}
<section className="mb-10">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Agenten im Detail</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Compliance Advisor */}
<div className="bg-white border border-gray-200 rounded-2xl p-6">
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 rounded-xl bg-purple-600 flex items-center justify-center text-white">
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5}
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
</div>
<h3 className="font-semibold text-gray-900">Compliance Advisor</h3>
</div>
<ul className="space-y-2 text-sm text-gray-600">
<li className="flex items-start gap-2">
<span className="text-purple-600 mt-0.5">&#8226;</span>
Multi-Collection RAG (6 Sammlungen parallel)
</li>
<li className="flex items-start gap-2">
<span className="text-purple-600 mt-0.5">&#8226;</span>
Laender-Filter: DE, AT, CH, EU
</li>
<li className="flex items-start gap-2">
<span className="text-purple-600 mt-0.5">&#8226;</span>
Streaming-Antworten (Ollama)
</li>
<li className="flex items-start gap-2">
<span className="text-purple-600 mt-0.5">&#8226;</span>
Quellenschutz: Keine Collection-Namen preisgeben
</li>
<li className="flex items-start gap-2">
<span className="text-purple-600 mt-0.5">&#8226;</span>
IFRS-Besonderheit: Nur EU-endorsed Standards
</li>
</ul>
<div className="mt-4 pt-3 border-t border-gray-100">
<code className="text-xs text-gray-500">API: POST /api/sdk/compliance-advisor/chat</code>
</div>
</div>
{/* Drafting Agent */}
<div className="bg-white border border-gray-200 rounded-2xl p-6">
<div className="flex items-center gap-3 mb-4">
<div className="w-10 h-10 rounded-xl bg-blue-600 flex items-center justify-center text-white">
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5}
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</div>
<h3 className="font-semibold text-gray-900">Drafting Agent</h3>
</div>
<ul className="space-y-2 text-sm text-gray-600">
<li className="flex items-start gap-2">
<span className="text-blue-600 mt-0.5">&#8226;</span>
<strong>Explain</strong>: Fragen verstaendlich beantworten
</li>
<li className="flex items-start gap-2">
<span className="text-blue-600 mt-0.5">&#8226;</span>
<strong>Ask</strong>: Luecken analysieren, gezielte Fragen
</li>
<li className="flex items-start gap-2">
<span className="text-blue-600 mt-0.5">&#8226;</span>
<strong>Draft</strong>: Dokument-Sections entwerfen (JSON)
</li>
<li className="flex items-start gap-2">
<span className="text-blue-600 mt-0.5">&#8226;</span>
<strong>Validate</strong>: Cross-Dokument-Konsistenz pruefen
</li>
<li className="flex items-start gap-2">
<span className="text-blue-600 mt-0.5">&#8226;</span>
SDK-State-Projection fuer token-effizienten Kontext
</li>
</ul>
<div className="mt-4 pt-3 border-t border-gray-100">
<code className="text-xs text-gray-500">API: POST /api/sdk/drafting-engine/chat</code>
</div>
</div>
</div>
</section>
{/* SOUL System */}
<section className="mb-10">
<h2 className="text-lg font-semibold text-gray-900 mb-4">SOUL-File System</h2>
<div className="bg-white border border-gray-200 rounded-2xl p-6">
<p className="text-sm text-gray-600 mb-4">
Jeder Agent hat eine <code className="bg-gray-100 px-1.5 py-0.5 rounded text-purple-700">.soul.md</code> Datei,
die seinen System-Prompt definiert. Diese Datei kann ueber die Agent-Detail-Seite live bearbeitet werden.
Aenderungen werden nach 30 Sekunden (TTL-Cache) wirksam.
</p>
<div className="bg-gray-50 rounded-xl p-4 font-mono text-sm text-gray-700">
<div>agent-core/</div>
<div className="ml-4">soul/</div>
<div className="ml-8 text-purple-700">compliance-advisor.soul.md</div>
<div className="ml-8 text-blue-700">drafting-agent.soul.md</div>
<div className="ml-8 text-gray-400">.backups/</div>
<div className="ml-12 text-gray-400">compliance-advisor-1709567890123.soul.md</div>
<div className="ml-12 text-gray-400">...</div>
</div>
</div>
</section>
{/* RAG Collections */}
<section className="mb-10">
<h2 className="text-lg font-semibold text-gray-900 mb-4">RAG-Sammlungen</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{[
{ name: 'bp_compliance_gesetze', desc: 'DSGVO, BDSG, AI Act, TTDSG, nationale Gesetze', icon: '&#9878;' },
{ name: 'bp_compliance_ce', desc: 'EU Maschinenverordnung, Blue Guide, CE-Kennzeichnung', icon: '&#9881;' },
{ name: 'bp_compliance_datenschutz', desc: 'DSK-Kurzpapiere, SDM, EDPB Guidelines', icon: '&#128274;' },
{ name: 'bp_dsfa_corpus', desc: 'DSFA-Listen, Muss-Listen der Aufsichtsbehoerden', icon: '&#128203;' },
{ name: 'bp_compliance_recht', desc: 'WP248, EU-Verordnungen (DORA, MiCA, etc.)', icon: '&#9878;' },
{ name: 'bp_legal_templates', desc: 'Vorlagen fuer Datenschutz-Dokumente', icon: '&#128196;' },
].map(col => (
<div key={col.name} className="bg-white border border-gray-200 rounded-xl p-4">
<div className="flex items-center gap-2 mb-2">
<span dangerouslySetInnerHTML={{ __html: col.icon }} />
<code className="text-xs font-medium text-gray-700">{col.name}</code>
</div>
<p className="text-xs text-gray-500">{col.desc}</p>
</div>
))}
</div>
</section>
{/* LLM Config */}
<section>
<h2 className="text-lg font-semibold text-gray-900 mb-4">LLM-Konfiguration</h2>
<div className="bg-white border border-gray-200 rounded-2xl p-6">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div>
<div className="text-xs text-gray-500 mb-1">Modell</div>
<div className="font-medium text-gray-900">qwen2.5vl:32b</div>
</div>
<div>
<div className="text-xs text-gray-500 mb-1">Backend</div>
<div className="font-medium text-gray-900">Ollama</div>
</div>
<div>
<div className="text-xs text-gray-500 mb-1">Temperatur</div>
<div className="font-medium text-gray-900">0.2 - 0.3</div>
</div>
<div>
<div className="text-xs text-gray-500 mb-1">Max Tokens</div>
<div className="font-medium text-gray-900">8.192 - 16.384</div>
</div>
</div>
</div>
</section>
</div>
)
}

View File

@@ -0,0 +1,224 @@
'use client'
import React, { useEffect, useState, useCallback } from 'react'
import Link from 'next/link'
interface AgentData {
id: string
name: string
description: string
color: string
icon: string
status: 'active' | 'inactive' | 'error'
version: string
stats: {
sessionsToday: number
avgResponseTime: string
successRate: string
}
}
interface AgentsResponse {
agents: AgentData[]
stats: {
total: number
active: number
inactive: number
error: number
totalSessions: number
avgResponseTime: string
}
timestamp: string
}
const ShieldIcon = () => (
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5}
d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
)
const PencilIcon = () => (
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5}
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
)
function getAgentIcon(icon: string) {
switch (icon) {
case 'shield': return <ShieldIcon />
case 'pencil': return <PencilIcon />
default: return <ShieldIcon />
}
}
function StatusBadge({ status }: { status: string }) {
const colors = {
active: 'bg-green-100 text-green-700',
inactive: 'bg-gray-100 text-gray-600',
error: 'bg-red-100 text-red-700',
}
const labels = { active: 'Aktiv', inactive: 'Inaktiv', error: 'Fehler' }
return (
<span className={`px-2 py-0.5 rounded-full text-xs font-medium ${colors[status as keyof typeof colors] || colors.inactive}`}>
{labels[status as keyof typeof labels] || status}
</span>
)
}
export default function AgentsDashboardPage() {
const [data, setData] = useState<AgentsResponse | null>(null)
const [loading, setLoading] = useState(true)
const loadAgents = useCallback(async () => {
try {
const res = await fetch('/api/sdk/agents')
if (res.ok) {
setData(await res.json())
}
} catch (err) {
console.error('Failed to load agents:', err)
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
loadAgents()
const interval = setInterval(loadAgents, 30_000)
return () => clearInterval(interval)
}, [loadAgents])
if (loading) {
return (
<div className="p-8">
<div className="animate-pulse space-y-4">
<div className="h-8 bg-gray-200 rounded w-64" />
<div className="grid grid-cols-3 gap-4">
{[1, 2, 3].map(i => <div key={i} className="h-24 bg-gray-200 rounded-xl" />)}
</div>
</div>
</div>
)
}
const stats = data?.stats
const agents = data?.agents || []
return (
<div className="p-8 max-w-6xl">
{/* Header */}
<div className="mb-8">
<h1 className="text-2xl font-bold text-gray-900">Compliance-Agenten</h1>
<p className="text-gray-500 mt-1">
Verwaltung und Konfiguration der KI-Agenten im Compliance SDK
</p>
</div>
{/* Quick Links */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-8">
<Link href="/sdk/agents/architecture" className="flex items-center gap-2 px-4 py-3 bg-white border border-gray-200 rounded-xl hover:border-purple-300 hover:bg-purple-50 transition-colors text-sm">
<svg className="w-5 h-5 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2" />
</svg>
Architektur
</Link>
<Link href="/sdk/agents/sessions" className="flex items-center gap-2 px-4 py-3 bg-white border border-gray-200 rounded-xl hover:border-purple-300 hover:bg-purple-50 transition-colors text-sm">
<svg className="w-5 h-5 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
</svg>
Sessions
</Link>
<Link href="/sdk/agents/statistics" className="flex items-center gap-2 px-4 py-3 bg-white border border-gray-200 rounded-xl hover:border-purple-300 hover:bg-purple-50 transition-colors text-sm">
<svg className="w-5 h-5 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
Statistiken
</Link>
<Link href="/sdk/document-generator" className="flex items-center gap-2 px-4 py-3 bg-white border border-gray-200 rounded-xl hover:border-purple-300 hover:bg-purple-50 transition-colors text-sm">
<svg className="w-5 h-5 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
Dokument-Generator
</Link>
</div>
{/* Stats Bar */}
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4 mb-8">
<div className="bg-white border border-gray-200 rounded-xl px-4 py-3">
<div className="text-2xl font-bold text-gray-900">{stats?.total || 0}</div>
<div className="text-xs text-gray-500">Agenten gesamt</div>
</div>
<div className="bg-white border border-gray-200 rounded-xl px-4 py-3">
<div className="text-2xl font-bold text-green-600">{stats?.active || 0}</div>
<div className="text-xs text-gray-500">Aktiv</div>
</div>
<div className="bg-white border border-gray-200 rounded-xl px-4 py-3">
<div className="text-2xl font-bold text-red-600">{stats?.error || 0}</div>
<div className="text-xs text-gray-500">Fehler</div>
</div>
<div className="bg-white border border-gray-200 rounded-xl px-4 py-3">
<div className="text-2xl font-bold text-gray-900">{stats?.totalSessions || 0}</div>
<div className="text-xs text-gray-500">Sessions heute</div>
</div>
<div className="bg-white border border-gray-200 rounded-xl px-4 py-3">
<div className="text-2xl font-bold text-gray-900">{stats?.avgResponseTime || '—'}</div>
<div className="text-xs text-gray-500">Avg. Antwortzeit</div>
</div>
<div className="bg-white border border-gray-200 rounded-xl px-4 py-3">
<div className="text-2xl font-bold text-gray-900">6</div>
<div className="text-xs text-gray-500">RAG-Sammlungen</div>
</div>
</div>
{/* Agent Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{agents.map((agent) => (
<Link
key={agent.id}
href={`/sdk/agents/${agent.id}`}
className="bg-white border border-gray-200 rounded-2xl p-6 hover:border-purple-300 hover:shadow-lg transition-all group"
>
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<div
className="w-12 h-12 rounded-xl flex items-center justify-center text-white"
style={{ backgroundColor: agent.color }}
>
{getAgentIcon(agent.icon)}
</div>
<div>
<h2 className="font-semibold text-gray-900 group-hover:text-purple-700 transition-colors">
{agent.name}
</h2>
<div className="text-xs text-gray-400">v{agent.version}</div>
</div>
</div>
<StatusBadge status={agent.status} />
</div>
<p className="text-sm text-gray-600 mb-4 line-clamp-2">
{agent.description}
</p>
<div className="grid grid-cols-3 gap-3 pt-3 border-t border-gray-100">
<div>
<div className="text-lg font-semibold text-gray-900">{agent.stats.sessionsToday}</div>
<div className="text-xs text-gray-500">Sessions</div>
</div>
<div>
<div className="text-lg font-semibold text-gray-900">{agent.stats.avgResponseTime}</div>
<div className="text-xs text-gray-500">Avg. Zeit</div>
</div>
<div>
<div className="text-lg font-semibold text-gray-900">{agent.stats.successRate}</div>
<div className="text-xs text-gray-500">Erfolgsrate</div>
</div>
</div>
</Link>
))}
</div>
</div>
)
}

View File

@@ -0,0 +1,34 @@
'use client'
import React from 'react'
import Link from 'next/link'
export default function AgentSessionsPage() {
return (
<div className="p-8 max-w-5xl">
<div className="flex items-center gap-4 mb-8">
<Link href="/sdk/agents" className="text-gray-400 hover:text-gray-600 transition-colors">
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</Link>
<div>
<h1 className="text-2xl font-bold text-gray-900">Agent-Sessions</h1>
<p className="text-gray-500 mt-1">Chat-Verlaeufe und Session-Management</p>
</div>
</div>
<div className="bg-white border border-gray-200 rounded-xl p-12 text-center">
<svg className="w-20 h-20 text-gray-300 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5}
d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
</svg>
<h2 className="text-xl font-medium text-gray-900 mb-2">Sessions-Tracking</h2>
<p className="text-gray-500 max-w-md mx-auto">
Das Session-Tracking fuer Compliance-Agenten wird in einer zukuenftigen Version implementiert.
Hier werden Chat-Verlaeufe, Antwortqualitaet und Nutzer-Feedback angezeigt.
</p>
</div>
</div>
)
}

View File

@@ -0,0 +1,34 @@
'use client'
import React from 'react'
import Link from 'next/link'
export default function AgentStatisticsPage() {
return (
<div className="p-8 max-w-5xl">
<div className="flex items-center gap-4 mb-8">
<Link href="/sdk/agents" className="text-gray-400 hover:text-gray-600 transition-colors">
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</Link>
<div>
<h1 className="text-2xl font-bold text-gray-900">Agent-Statistiken</h1>
<p className="text-gray-500 mt-1">Performance-Metriken und Nutzungsanalysen</p>
</div>
</div>
<div className="bg-white border border-gray-200 rounded-xl p-12 text-center">
<svg className="w-20 h-20 text-gray-300 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5}
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
<h2 className="text-xl font-medium text-gray-900 mb-2">Agent-Statistiken</h2>
<p className="text-gray-500 max-w-md mx-auto">
Detaillierte Statistiken wie Antwortzeiten, Erfolgsraten, haeufigste Themen und
RAG-Trefferquoten werden in einer zukuenftigen Version implementiert.
</p>
</div>
</div>
)
}

View File

@@ -0,0 +1,37 @@
/**
* GET /api/sdk/agents/[agentId] — Agent-Detail mit SOUL-Content
*/
import { NextRequest, NextResponse } from 'next/server'
import { getAgentById } from '@/lib/sdk/agents/agent-registry'
import { readSoulFile, getSoulFileStats, soulFileExists } from '@/lib/sdk/agents/soul-reader'
export async function GET(
_request: NextRequest,
{ params }: { params: Promise<{ agentId: string }> }
) {
try {
const { agentId } = await params
const agent = getAgentById(agentId)
if (!agent) {
return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
}
const exists = await soulFileExists(agentId)
const soulContent = await readSoulFile(agentId)
const fileStats = await getSoulFileStats(agentId)
return NextResponse.json({
...agent,
status: exists ? agent.status : 'error',
soulContent: soulContent || '',
createdAt: fileStats?.createdAt || null,
updatedAt: fileStats?.updatedAt || null,
fileSize: fileStats?.size || 0,
})
} catch (error) {
console.error('Error fetching agent detail:', error)
return NextResponse.json({ error: 'Failed to fetch agent' }, { status: 500 })
}
}

View File

@@ -0,0 +1,84 @@
/**
* GET/PUT /api/sdk/agents/[agentId]/soul — SOUL-Datei lesen/schreiben
*
* GET: Content + Metadaten, ?history=true fuer Backup-Versionen
* PUT: Backup erstellen -> neuen Content schreiben -> Cache invalidieren
*/
import { NextRequest, NextResponse } from 'next/server'
import { getAgentById } from '@/lib/sdk/agents/agent-registry'
import {
readSoulFile,
writeSoulFile,
listSoulBackups,
getSoulFileStats,
} from '@/lib/sdk/agents/soul-reader'
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ agentId: string }> }
) {
try {
const { agentId } = await params
const agent = getAgentById(agentId)
if (!agent) {
return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
}
const showHistory = request.nextUrl.searchParams.get('history') === 'true'
if (showHistory) {
const backups = await listSoulBackups(agentId)
return NextResponse.json({ agentId, backups })
}
const content = await readSoulFile(agentId)
const fileStats = await getSoulFileStats(agentId)
return NextResponse.json({
agentId,
content: content || '',
updatedAt: fileStats?.updatedAt || null,
size: fileStats?.size || 0,
})
} catch (error) {
console.error('Error reading SOUL file:', error)
return NextResponse.json({ error: 'Failed to read SOUL file' }, { status: 500 })
}
}
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ agentId: string }> }
) {
try {
const { agentId } = await params
const agent = getAgentById(agentId)
if (!agent) {
return NextResponse.json({ error: 'Agent not found' }, { status: 404 })
}
const body = await request.json()
const { content } = body
if (typeof content !== 'string' || content.trim().length === 0) {
return NextResponse.json({ error: 'Content is required' }, { status: 400 })
}
await writeSoulFile(agentId, content)
const fileStats = await getSoulFileStats(agentId)
return NextResponse.json({
agentId,
updatedAt: fileStats?.updatedAt || new Date().toISOString(),
size: fileStats?.size || content.length,
message: 'SOUL file updated successfully',
})
} catch (error) {
console.error('Error writing SOUL file:', error)
return NextResponse.json({ error: 'Failed to write SOUL file' }, { status: 500 })
}
}

View File

@@ -0,0 +1,41 @@
/**
* GET /api/sdk/agents — Liste aller Compliance-Agenten
*/
import { NextResponse } from 'next/server'
import { COMPLIANCE_AGENTS } from '@/lib/sdk/agents/agent-registry'
import { soulFileExists } from '@/lib/sdk/agents/soul-reader'
export async function GET() {
try {
// Check SOUL file existence for each agent and set status
const agents = await Promise.all(
COMPLIANCE_AGENTS.map(async (agent) => {
const exists = await soulFileExists(agent.id)
return {
...agent,
status: exists ? agent.status : 'error' as const,
}
})
)
const activeCount = agents.filter(a => a.status === 'active').length
const errorCount = agents.filter(a => a.status === 'error').length
return NextResponse.json({
agents,
stats: {
total: agents.length,
active: activeCount,
inactive: agents.length - activeCount - errorCount,
error: errorCount,
totalSessions: 0,
avgResponseTime: '—',
},
timestamp: new Date().toISOString(),
})
} catch (error) {
console.error('Error fetching agents:', error)
return NextResponse.json({ error: 'Failed to fetch agents' }, { status: 500 })
}
}

View File

@@ -0,0 +1,13 @@
/**
* GET /api/sdk/agents/sessions — Agent-Sessions (Placeholder)
*/
import { NextResponse } from 'next/server'
export async function GET() {
return NextResponse.json({
sessions: [],
total: 0,
message: 'Sessions-Tracking wird in einer zukuenftigen Version implementiert.',
})
}

View File

@@ -0,0 +1,18 @@
/**
* GET /api/sdk/agents/statistics — Agent-Statistiken (Placeholder)
*/
import { NextResponse } from 'next/server'
export async function GET() {
return NextResponse.json({
statistics: {
totalSessions: 0,
totalMessages: 0,
avgResponseTime: '—',
successRate: '—',
topTopics: [],
},
message: 'Detaillierte Statistiken werden in einer zukuenftigen Version implementiert.',
})
}

View File

@@ -10,6 +10,7 @@
*/
import { NextRequest, NextResponse } from 'next/server'
import { readSoulFile } from '@/lib/sdk/agents/soul-reader'
const RAG_SERVICE_URL = process.env.RAG_SERVICE_URL || 'http://rag-service:8097'
const OLLAMA_URL = process.env.OLLAMA_URL || 'http://host.docker.internal:11434'
@@ -27,99 +28,18 @@ const COMPLIANCE_COLLECTIONS = [
type Country = 'DE' | 'AT' | 'CH' | 'EU'
// SOUL system prompt (from agent-core/soul/compliance-advisor.soul.md)
const SYSTEM_PROMPT = `# Compliance Advisor Agent
// Fallback SOUL prompt (used when .soul.md file is unavailable)
const FALLBACK_SYSTEM_PROMPT = `# Compliance Advisor Agent
## Identitaet
Du bist der BreakPilot Compliance-Berater. Du hilfst Nutzern des AI Compliance SDK,
Datenschutz- und Compliance-Fragen in verstaendlicher Sprache zu beantworten.
Du bist kein Anwalt und gibst keine Rechtsberatung, sondern orientierst dich an
offiziellen Quellen und gibst praxisnahe Hinweise.
## Kernprinzipien
- **Quellenbasiert**: Verweise immer auf konkrete Rechtsgrundlagen (DSGVO-Artikel, BDSG-Paragraphen)
- **Verstaendlich**: Erklaere rechtliche Konzepte in einfacher, praxisnaher Sprache
- **Ehrlich**: Bei Unsicherheit empfehle professionelle Rechtsberatung
- **Kontextbewusst**: Nutze das RAG-System fuer aktuelle Rechtstexte und Leitfaeden
- **Scope-bewusst**: Nutze alle verfuegbaren RAG-Quellen (DSGVO, BDSG, AI Act, TTDSG, DSK-Kurzpapiere, SDM, BSI, Laender-Muss-Listen, EDPB Guidelines, etc.) AUSSER NIBIS-Dokumenten.
## Kompetenzbereich
- DSGVO Art. 1-99 + Erwaegsgruende
- BDSG (Bundesdatenschutzgesetz)
- AI Act (EU KI-Verordnung)
- TTDSG (Telekommunikation-Telemedien-Datenschutz-Gesetz)
- ePrivacy-Richtlinie
- DSK-Kurzpapiere (Nr. 1-20)
- SDM (Standard-Datenschutzmodell) V3.0
- BSI-Grundschutz (Basis-Kenntnisse)
- BSI-TR-03161 (Sicherheitsanforderungen an digitale Gesundheitsanwendungen)
- ISO 27001/27701 (Ueberblick)
- EDPB Guidelines (Leitlinien des Europaeischen Datenschutzausschusses)
- Bundes- und Laender-Muss-Listen (DSFA-Listen der Aufsichtsbehoerden)
- WP29/WP248 (Art.-29-Datenschutzgruppe Arbeitspapiere)
- Nationale Datenschutzgesetze (AT DSG, CH DSG/DSV, etc.)
- EU-Verordnungen (DORA, MiCA, Data Act, EHDS, PSD2, AMLR, etc.)
- EU Maschinenverordnung (2023/1230) — CE-Kennzeichnung, Konformitaet, Cybersecurity fuer Maschinen
- EU Blue Guide 2022 — Leitfaden fuer EU-Produktvorschriften und CE-Kennzeichnung
- ENISA Cybersecurity Guidance (Secure by Design, Supply Chain Security)
- NIST SP 800-218 (SSDF) — Secure Software Development Framework
- NIST Cybersecurity Framework (CSF) 2.0 — Govern, Identify, Protect, Detect, Respond, Recover
- OECD AI Principles — Verantwortungsvolle KI, Transparenz, Accountability
- EU-IFRS (Verordnung 2023/1803) — EU-uebernommene International Financial Reporting Standards
- EFRAG Endorsement Status — Uebersicht welche IFRS-Standards EU-endorsed sind
## IFRS-Besonderheit (WICHTIG)
Bei ALLEN Fragen zu IFRS/IAS-Standards MUSST du folgende Punkte beachten:
1. Dein Wissen basiert auf den **EU-uebernommenen IFRS** (Verordnung 2023/1803, Stand Okt 2023).
2. Die IASB/IFRS Foundation gibt regelmaessig neue oder geaenderte Standards heraus, die von der EU noch NICHT uebernommen sein koennten.
3. Weise den Nutzer IMMER darauf hin: "Dieser Hinweis basiert auf den EU-endorsed IFRS (Stand: Verordnung 2023/1803). Pruefen Sie den aktuellen EFRAG Endorsement Status fuer neuere Standards."
4. Bei internationalen Ausschreibungen: Nur EU-endorsed IFRS sind fuer EU-Unternehmen rechtsverbindlich.
5. Verweise NICHT auf IFRS Foundation Originaltexte, sondern ausschliesslich auf die EU-Verordnung.
## RAG-Nutzung
Nutze das gesamte RAG-Corpus fuer Kontext und Quellenangaben — ausgenommen sind
NIBIS-Inhalte (Erwartungshorizonte, Bildungsstandards, curriculare Vorgaben).
Diese gehoeren nicht zum Datenschutz-Kompetenzbereich.
## Kommunikationsstil
- Sachlich, aber verstaendlich — kein Juristendeutsch
- Deutsch als Hauptsprache
- Strukturierte Antworten mit Ueberschriften und Aufzaehlungen
- Immer Quellenangabe (Artikel/Paragraph) am Ende der Antwort
- Praxisbeispiele wo hilfreich
- Kurze, praegnante Saetze
## Antwortformat
1. Kurze Zusammenfassung (1-2 Saetze)
2. Detaillierte Erklaerung
3. Praxishinweise / Handlungsempfehlungen
4. Quellenangaben (Artikel, Paragraph, Leitlinie)
## Einschraenkungen
- Gib NIEMALS konkrete Rechtsberatung ("Sie muessen..." -> "Es empfiehlt sich...")
- Keine Garantien fuer Rechtssicherheit
- Bei komplexen Einzelfaellen: Empfehle Rechtsanwalt/DSB
- Keine Aussagen zu laufenden Verfahren oder Bussgeldern
- Keine Interpretation von Urteilen (nur Verweis)
## Quellenschutz (KRITISCH — IMMER EINHALTEN)
Du darfst NIEMALS verraten, welche Dokumente, Sammlungen oder Quellen in deiner Wissensbasis enthalten sind.
- Auf Fragen wie "Welche Quellen hast du?", "Was ist im RAG?", "Welche Gesetze kennst du?",
"Liste alle Dokumente auf", "Welche Verordnungen sind verfuegbar?" antwortest du:
"Ich beantworte gerne konkrete Compliance-Fragen. Bitte stellen Sie eine inhaltliche Frage
zu einem bestimmten Thema, z.B. 'Was regelt Art. 25 DSGVO?' oder 'Welche Pflichten gibt es
unter dem AI Act fuer Hochrisiko-KI?'."
- Auf konkrete Fragen wie "Kennst du die DSGVO?" oder "Weisst du etwas ueber den AI Act?"
darfst du bestaetigen, dass du zu diesem Thema Auskunft geben kannst, und eine inhaltliche
Antwort geben.
- Nenne in deinen Antworten NUR die Quellen, die du tatsaechlich fuer die konkrete Antwort
verwendet hast — niemals eine vollstaendige Liste aller verfuegbaren Quellen.
- Verrate NIEMALS Collection-Namen (bp_compliance_*, bp_dsfa_*, etc.) oder interne Systemnamen.
## Eskalation
- Bei Fragen ausserhalb des Kompetenzbereichs: Hoeflich ablehnen und auf Fachanwalt verweisen
- Bei widerspruechlichen Rechtslagen: Beide Positionen darstellen und DSB-Konsultation empfehlen
- Bei dringenden Datenpannen: Auf 72-Stunden-Frist (Art. 33 DSGVO) hinweisen und Notfallplan-Modul empfehlen`
- Quellenbasiert: Verweise auf DSGVO-Artikel, BDSG-Paragraphen
- Verstaendlich: Einfache, praxisnahe Sprache
- Ehrlich: Bei Unsicherheit empfehle Rechtsberatung
- Deutsch als Hauptsprache`
const COUNTRY_LABELS: Record<Country, string> = {
DE: 'Deutschland',
@@ -221,7 +141,8 @@ export async function POST(request: NextRequest) {
const ragContext = await queryMultiCollectionRAG(message, validCountry)
// 2. Build system prompt with RAG context + country
let systemContent = SYSTEM_PROMPT
const soulPrompt = await readSoulFile('compliance-advisor')
let systemContent = soulPrompt || FALLBACK_SYSTEM_PROMPT
if (validCountry) {
const countryLabel = COUNTRY_LABELS[validCountry]

View File

@@ -8,27 +8,22 @@
import { NextRequest, NextResponse } from 'next/server'
import { queryRAG } from '@/lib/sdk/drafting-engine/rag-query'
import { readSoulFile } from '@/lib/sdk/agents/soul-reader'
const OLLAMA_URL = process.env.OLLAMA_URL || 'http://host.docker.internal:11434'
const LLM_MODEL = process.env.COMPLIANCE_LLM_MODEL || 'qwen2.5vl:32b'
// SOUL System Prompt (from agent-core/soul/drafting-agent.soul.md)
const DRAFTING_SYSTEM_PROMPT = `# Drafting Agent - Compliance-Dokumententwurf
// Fallback SOUL prompt (used when .soul.md file is unavailable)
const FALLBACK_DRAFTING_PROMPT = `# Drafting Agent - Compliance-Dokumententwurf
## Identitaet
Du bist der BreakPilot Drafting Agent. Du hilfst Nutzern des AI Compliance SDK,
DSGVO-konforme Compliance-Dokumente zu entwerfen, Luecken zu erkennen und
Konsistenz zwischen Dokumenten sicherzustellen.
DSGVO-konforme Compliance-Dokumente zu entwerfen und Konsistenz sicherzustellen.
## Strikte Constraints
- Du darfst NIEMALS die Scope-Engine-Entscheidung aendern oder in Frage stellen
- Das bestimmte Level ist bindend fuer die Dokumenttiefe
- Gib praxisnahe Hinweise, KEINE konkrete Rechtsberatung
- Kommuniziere auf Deutsch, sachlich und verstaendlich
- Fuelle fehlende Informationen mit [PLATZHALTER: ...] Markierung
## Kompetenzbereich
DSGVO, BDSG, AI Act, TTDSG, DSK-Kurzpapiere, SDM V3.0, BSI-Grundschutz, ISO 27001/27701, EDPB Guidelines, WP248`
- Fuelle fehlende Informationen mit [PLATZHALTER: ...] Markierung`
export async function POST(request: NextRequest) {
try {
@@ -49,7 +44,8 @@ export async function POST(request: NextRequest) {
const ragContext = await queryRAG(message)
// 2. Build system prompt with mode-specific instructions + state projection
let systemContent = DRAFTING_SYSTEM_PROMPT
const soulPrompt = await readSoulFile('drafting-agent')
let systemContent = soulPrompt || FALLBACK_DRAFTING_PROMPT
// Mode-specific instructions
const modeInstructions: Record<string, string> = {

View File

@@ -590,6 +590,18 @@ export function SDKSidebar({ collapsed = false, onCollapsedChange }: SDKSidebarP
isActive={pathname === '/sdk/architecture'}
collapsed={collapsed}
/>
<AdditionalModuleItem
href="/sdk/agents"
icon={
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
}
label="Agenten"
isActive={pathname?.startsWith('/sdk/agents') ?? false}
collapsed={collapsed}
/>
<AdditionalModuleItem
href="/sdk/catalog-manager"
icon={

View File

@@ -0,0 +1,56 @@
/**
* Agent Registry — static configuration for the 2 compliance agents.
*/
export interface AgentConfig {
id: string
name: string
description: string
soulFile: string
color: string
icon: string
status: 'active' | 'inactive' | 'error'
version: string
stats: {
sessionsToday: number
avgResponseTime: string
successRate: string
}
}
export const COMPLIANCE_AGENTS: AgentConfig[] = [
{
id: 'compliance-advisor',
name: 'Compliance Advisor',
description: 'RAG-gestuetzter Chat-Agent fuer DSGVO, AI Act und Compliance-Fragen. Durchsucht 6 Sammlungen mit Multi-Collection-RAG und liefert quellenbasierte Antworten mit Streaming.',
soulFile: 'compliance-advisor.soul.md',
color: '#7c3aed', // purple-600
icon: 'shield',
status: 'active',
version: '1.0.0',
stats: {
sessionsToday: 0,
avgResponseTime: '—',
successRate: '—',
},
},
{
id: 'drafting-agent',
name: 'Drafting Agent',
description: '4-Modi Dokumenten-Agent (Explain, Ask, Draft, Validate). Entwirft DSGVO-konforme Compliance-Dokumente, erkennt Luecken und prueft Cross-Dokument-Konsistenz.',
soulFile: 'drafting-agent.soul.md',
color: '#2563eb', // blue-600
icon: 'pencil',
status: 'active',
version: '1.0.0',
stats: {
sessionsToday: 0,
avgResponseTime: '—',
successRate: '—',
},
},
]
export function getAgentById(id: string): AgentConfig | undefined {
return COMPLIANCE_AGENTS.find(a => a.id === id)
}

View File

@@ -0,0 +1,131 @@
/**
* SOUL File Reader — reads/writes .soul.md files from agent-core/soul/
* with 30s TTL cache and backup support.
*/
import fs from 'fs/promises'
import path from 'path'
const AGENT_CORE_PATH = process.env.AGENT_CORE_PATH || path.join(process.cwd(), 'agent-core')
const SOUL_DIR = path.join(AGENT_CORE_PATH, 'soul')
const BACKUPS_DIR = path.join(SOUL_DIR, '.backups')
// 30s TTL cache
const cache = new Map<string, { content: string; timestamp: number }>()
const CACHE_TTL = 30_000
function getSoulFilePath(agentId: string): string {
// Prevent path traversal
const safe = agentId.replace(/[^a-z0-9-]/g, '')
return path.join(SOUL_DIR, `${safe}.soul.md`)
}
/**
* Read a SOUL file with 30s cache
*/
export async function readSoulFile(agentId: string): Promise<string | null> {
const cached = cache.get(agentId)
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return cached.content
}
try {
const filePath = getSoulFilePath(agentId)
const content = await fs.readFile(filePath, 'utf-8')
cache.set(agentId, { content, timestamp: Date.now() })
return content
} catch {
return null
}
}
/**
* Write a SOUL file — creates backup first, then writes new content
*/
export async function writeSoulFile(agentId: string, content: string): Promise<void> {
const filePath = getSoulFilePath(agentId)
// Ensure backups dir exists
await fs.mkdir(BACKUPS_DIR, { recursive: true })
// Create backup of current file
try {
const current = await fs.readFile(filePath, 'utf-8')
const backupName = `${agentId}-${Date.now()}.soul.md`
await fs.writeFile(path.join(BACKUPS_DIR, backupName), current, 'utf-8')
} catch {
// No existing file to backup
}
// Write new content
await fs.writeFile(filePath, content, 'utf-8')
// Invalidate cache
cache.delete(agentId)
}
/**
* List backup versions for an agent
*/
export async function listSoulBackups(agentId: string): Promise<Array<{ filename: string; timestamp: number; size: number }>> {
try {
const files = await fs.readdir(BACKUPS_DIR)
const prefix = `${agentId}-`
const backups: Array<{ filename: string; timestamp: number; size: number }> = []
for (const file of files) {
if (!file.startsWith(prefix) || !file.endsWith('.soul.md')) continue
const tsStr = file.slice(prefix.length, -'.soul.md'.length)
const ts = parseInt(tsStr, 10)
if (isNaN(ts)) continue
const stat = await fs.stat(path.join(BACKUPS_DIR, file))
backups.push({ filename: file, timestamp: ts, size: stat.size })
}
return backups.sort((a, b) => b.timestamp - a.timestamp)
} catch {
return []
}
}
/**
* Read a specific backup file
*/
export async function readSoulBackup(filename: string): Promise<string | null> {
try {
// Prevent path traversal
const safe = path.basename(filename)
return await fs.readFile(path.join(BACKUPS_DIR, safe), 'utf-8')
} catch {
return null
}
}
/**
* Check if SOUL file exists
*/
export async function soulFileExists(agentId: string): Promise<boolean> {
try {
await fs.access(getSoulFilePath(agentId))
return true
} catch {
return false
}
}
/**
* Get SOUL file stats
*/
export async function getSoulFileStats(agentId: string): Promise<{ size: number; createdAt: string; updatedAt: string } | null> {
try {
const stat = await fs.stat(getSoulFilePath(agentId))
return {
size: stat.size,
createdAt: stat.birthtime.toISOString(),
updatedAt: stat.mtime.toISOString(),
}
} catch {
return null
}
}