feat: Legal Templates Service — eigene Vorlagen für Dokumentengenerator
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 32s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 18s
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 32s
CI / test-python-document-crawler (push) Successful in 21s
CI / test-python-dsms-gateway (push) Successful in 18s
Implementiert MIT-lizenzierte DSGVO-Templates (DSE, Impressum, AGB) in der eigenen PostgreSQL-Datenbank statt KLAUSUR_SERVICE-Abhängigkeit. - Migration 018: compliance_legal_templates Tabelle + 3 Seed-Templates - Routes: GET/POST/PUT/DELETE /legal-templates + /status + /sources - Registriert im bestehenden compliance catch-all Proxy (kein neuer Proxy) - searchTemplates.ts: eigenes Backend als Primary, RAG bleibt Fallback - ServiceMode-Banner: KLAUSUR_SERVICE-Referenz entfernt - Tests: 25 Python + 3 Vitest — alle grün Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -257,9 +257,9 @@ export default function DocumentGeneratorPage() {
|
||||
])
|
||||
setStatus(statusData)
|
||||
setSources(sourcesData)
|
||||
const hasKlausur = statusData !== null
|
||||
const hasTemplateDb = statusData !== null
|
||||
const hasRag = await fetch(`${RAG_PROXY}/regulations`).then(r => r.ok).catch(() => false)
|
||||
setServiceMode(hasKlausur ? 'full' : hasRag ? 'rag-only' : 'offline')
|
||||
setServiceMode(hasTemplateDb ? 'full' : hasRag ? 'rag-only' : 'offline')
|
||||
} catch {
|
||||
setServiceMode('offline')
|
||||
} finally {
|
||||
@@ -396,7 +396,7 @@ export default function DocumentGeneratorPage() {
|
||||
{/* Service mode banners */}
|
||||
{serviceMode === 'rag-only' && (
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3 text-sm text-yellow-800">
|
||||
⚠️ KLAUSUR_SERVICE nicht verfügbar — Suche läuft über RAG-Fallback
|
||||
⚠️ Template-Datenbank nicht erreichbar — Suche läuft über RAG-Fallback
|
||||
</div>
|
||||
)}
|
||||
{serviceMode === 'offline' && (
|
||||
|
||||
@@ -1,49 +1,51 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { searchTemplates } from './searchTemplates'
|
||||
|
||||
// jsdom doesn't define window.location.origin — stub it
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { origin: 'https://localhost' },
|
||||
writable: true,
|
||||
})
|
||||
|
||||
describe('searchTemplates', () => {
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('gibt Ergebnisse zurück wenn KLAUSUR_SERVICE verfügbar', async () => {
|
||||
it('gibt Ergebnisse zurück wenn eigenes Backend verfügbar', async () => {
|
||||
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve([{
|
||||
id: 't1',
|
||||
score: 0.9,
|
||||
text: 'Inhalt',
|
||||
document_title: 'DSE Template',
|
||||
template_type: 'privacy_policy',
|
||||
clause_category: null,
|
||||
language: 'de',
|
||||
jurisdiction: 'de',
|
||||
license_id: 'mit',
|
||||
license_name: 'MIT',
|
||||
license_url: null,
|
||||
attribution_required: false,
|
||||
attribution_text: null,
|
||||
source_name: 'Test',
|
||||
source_url: null,
|
||||
source_repo: null,
|
||||
placeholders: [],
|
||||
is_complete_document: true,
|
||||
is_modular: false,
|
||||
requires_customization: false,
|
||||
output_allowed: true,
|
||||
modification_allowed: true,
|
||||
distortion_prohibited: false,
|
||||
}]),
|
||||
json: () => Promise.resolve({
|
||||
templates: [{
|
||||
id: 't1',
|
||||
title: 'Datenschutzerklärung (DSGVO-konform)',
|
||||
document_type: 'privacy_policy',
|
||||
content: '# Datenschutzerklärung\n\n{{COMPANY_NAME}}',
|
||||
description: 'Vollständige DSE',
|
||||
language: 'de',
|
||||
jurisdiction: 'DE',
|
||||
license_id: 'mit',
|
||||
license_name: 'MIT License',
|
||||
source_name: 'BreakPilot Compliance',
|
||||
attribution_required: false,
|
||||
is_complete_document: true,
|
||||
placeholders: ['{{COMPANY_NAME}}', '{{CONTACT_EMAIL}}'],
|
||||
}],
|
||||
total: 1,
|
||||
}),
|
||||
}))
|
||||
|
||||
const results = await searchTemplates({ query: 'Datenschutzerklärung' })
|
||||
expect(results).toHaveLength(1)
|
||||
expect(results[0].documentTitle).toBe('DSE Template')
|
||||
expect(results[0].documentTitle).toBe('Datenschutzerklärung (DSGVO-konform)')
|
||||
expect(results[0].templateType).toBe('privacy_policy')
|
||||
expect(results[0].placeholders).toContain('{{COMPANY_NAME}}')
|
||||
expect((results[0] as any).source).toBe('db')
|
||||
})
|
||||
|
||||
it('fällt auf RAG zurück wenn KLAUSUR_SERVICE fehlschlägt', async () => {
|
||||
it('fällt auf RAG zurück wenn Backend fehlschlägt', async () => {
|
||||
vi.stubGlobal('fetch', vi.fn()
|
||||
.mockRejectedValueOnce(new Error('KLAUSUR down'))
|
||||
.mockRejectedValueOnce(new Error('Backend down'))
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
/**
|
||||
* Template search helpers for document-generator.
|
||||
*
|
||||
* Provides a two-tier search:
|
||||
* 1. Primary: KLAUSUR_SERVICE (curated legal templates with full metadata)
|
||||
* 2. Fallback: RAG proxy (ai-compliance-sdk regulation search)
|
||||
* Two-tier search:
|
||||
* 1. Primary: own backend (compliance_legal_templates DB) — MIT-licensed, curated
|
||||
* 2. Fallback: RAG proxy (ai-compliance-sdk regulation search — law texts)
|
||||
*/
|
||||
|
||||
import type { LegalTemplateResult } from '@/lib/sdk/types'
|
||||
|
||||
export const KLAUSUR_SERVICE_URL =
|
||||
process.env.NEXT_PUBLIC_KLAUSUR_SERVICE_URL || 'http://localhost:8086'
|
||||
|
||||
export const TEMPLATES_API = '/api/sdk/v1/compliance/legal-templates'
|
||||
export const RAG_PROXY = '/api/sdk/v1/rag'
|
||||
|
||||
export interface TemplateSearchParams {
|
||||
@@ -23,63 +21,34 @@ export interface TemplateSearchParams {
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for legal templates with automatic RAG fallback.
|
||||
* Search for legal templates.
|
||||
*
|
||||
* Tries KLAUSUR_SERVICE first (5 s timeout). If unavailable or returning an
|
||||
* error, falls back to the RAG proxy served by ai-compliance-sdk.
|
||||
* Tries own backend DB first (5 s timeout). Falls back to RAG proxy
|
||||
* (regulation texts — useful as reference, but not ready-made templates).
|
||||
* Returns an empty array if both services are down.
|
||||
*/
|
||||
export async function searchTemplates(
|
||||
params: TemplateSearchParams
|
||||
): Promise<LegalTemplateResult[]> {
|
||||
// 1. Primary: KLAUSUR_SERVICE
|
||||
// 1. Primary: own backend — compliance_legal_templates table
|
||||
try {
|
||||
const res = await fetch(`${KLAUSUR_SERVICE_URL}/api/v1/admin/templates/search`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
query: params.query,
|
||||
template_type: params.templateType,
|
||||
license_types: params.licenseTypes,
|
||||
language: params.language,
|
||||
jurisdiction: params.jurisdiction,
|
||||
limit: params.limit || 10,
|
||||
}),
|
||||
signal: AbortSignal.timeout(5000),
|
||||
})
|
||||
const url = new URL(TEMPLATES_API, window.location.origin)
|
||||
if (params.query) url.searchParams.set('query', params.query)
|
||||
if (params.templateType) url.searchParams.set('document_type', params.templateType)
|
||||
if (params.language) url.searchParams.set('language', params.language)
|
||||
url.searchParams.set('limit', String(params.limit || 20))
|
||||
url.searchParams.set('status', 'published')
|
||||
|
||||
const res = await fetch(url.toString(), { signal: AbortSignal.timeout(5000) })
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
return data.map((r: any) => ({
|
||||
id: r.id,
|
||||
score: r.score,
|
||||
text: r.text,
|
||||
documentTitle: r.document_title,
|
||||
templateType: r.template_type,
|
||||
clauseCategory: r.clause_category,
|
||||
language: r.language,
|
||||
jurisdiction: r.jurisdiction,
|
||||
licenseId: r.license_id,
|
||||
licenseName: r.license_name,
|
||||
licenseUrl: r.license_url,
|
||||
attributionRequired: r.attribution_required,
|
||||
attributionText: r.attribution_text,
|
||||
sourceName: r.source_name,
|
||||
sourceUrl: r.source_url,
|
||||
sourceRepo: r.source_repo,
|
||||
placeholders: r.placeholders || [],
|
||||
isCompleteDocument: r.is_complete_document,
|
||||
isModular: r.is_modular,
|
||||
requiresCustomization: r.requires_customization,
|
||||
outputAllowed: r.output_allowed ?? true,
|
||||
modificationAllowed: r.modification_allowed ?? true,
|
||||
distortionProhibited: r.distortion_prohibited ?? false,
|
||||
}))
|
||||
return (data.templates || []).map(mapTemplateToResult)
|
||||
}
|
||||
} catch {
|
||||
// KLAUSUR_SERVICE not reachable — fall through to RAG
|
||||
// Backend not reachable — fall through to RAG
|
||||
}
|
||||
|
||||
// 2. Fallback: RAG proxy
|
||||
// 2. Fallback: RAG proxy (Gesetzestexte — Referenz, kein fertiges Template)
|
||||
try {
|
||||
const res = await fetch(`${RAG_PROXY}/search`, {
|
||||
method: 'POST',
|
||||
@@ -122,19 +91,52 @@ export async function searchTemplates(
|
||||
return []
|
||||
}
|
||||
|
||||
function mapTemplateToResult(r: any): LegalTemplateResult {
|
||||
return {
|
||||
id: r.id,
|
||||
score: 1.0,
|
||||
text: r.content || '',
|
||||
documentTitle: r.title,
|
||||
templateType: r.document_type,
|
||||
clauseCategory: null,
|
||||
language: r.language,
|
||||
jurisdiction: r.jurisdiction,
|
||||
licenseId: r.license_id as any,
|
||||
licenseName: r.license_name,
|
||||
licenseUrl: null,
|
||||
attributionRequired: r.attribution_required ?? false,
|
||||
attributionText: null,
|
||||
sourceName: r.source_name,
|
||||
sourceUrl: null,
|
||||
sourceRepo: null,
|
||||
placeholders: r.placeholders || [],
|
||||
isCompleteDocument: r.is_complete_document ?? true,
|
||||
isModular: false,
|
||||
requiresCustomization: (r.placeholders || []).length > 0,
|
||||
outputAllowed: true,
|
||||
modificationAllowed: true,
|
||||
distortionProhibited: false,
|
||||
source: 'db' as const,
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTemplatesStatus(): Promise<any> {
|
||||
const response = await fetch(`${KLAUSUR_SERVICE_URL}/api/v1/admin/templates/status`, {
|
||||
signal: AbortSignal.timeout(5000),
|
||||
})
|
||||
if (!response.ok) return null
|
||||
return response.json()
|
||||
try {
|
||||
const res = await fetch(`${TEMPLATES_API}/status`, { signal: AbortSignal.timeout(5000) })
|
||||
if (!res.ok) return null
|
||||
return res.json()
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSources(): Promise<any[]> {
|
||||
const response = await fetch(`${KLAUSUR_SERVICE_URL}/api/v1/admin/templates/sources`, {
|
||||
signal: AbortSignal.timeout(5000),
|
||||
})
|
||||
if (!response.ok) return []
|
||||
const data = await response.json()
|
||||
return data.sources || []
|
||||
try {
|
||||
const res = await fetch(`${TEMPLATES_API}/sources`, { signal: AbortSignal.timeout(5000) })
|
||||
if (!res.ok) return []
|
||||
const data = await res.json()
|
||||
return data.sources || []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user