Services: Admin-Compliance, Backend-Compliance, AI-Compliance-SDK, Consent-SDK, Developer-Portal, PCA-Platform, DSMS Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
185 lines
6.1 KiB
TypeScript
185 lines
6.1 KiB
TypeScript
/**
|
|
* Drafting Engine Chat API
|
|
*
|
|
* Verbindet das DraftingEngineWidget mit dem LLM Backend.
|
|
* Unterstuetzt alle 4 Modi: explain, ask, draft, validate.
|
|
* Nutzt State-Projection fuer token-effiziente Kontextgabe.
|
|
*/
|
|
|
|
import { NextRequest, NextResponse } from 'next/server'
|
|
|
|
const KLAUSUR_SERVICE_URL = process.env.KLAUSUR_SERVICE_URL || 'http://klausur-service:8086'
|
|
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
|
|
|
|
## 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`
|
|
|
|
/**
|
|
* Query the RAG corpus for relevant documents
|
|
*/
|
|
async function queryRAG(query: string): Promise<string> {
|
|
try {
|
|
const url = `${KLAUSUR_SERVICE_URL}/api/v1/dsfa-rag/search?query=${encodeURIComponent(query)}&top_k=3`
|
|
const res = await fetch(url, {
|
|
headers: { 'Content-Type': 'application/json' },
|
|
signal: AbortSignal.timeout(10000),
|
|
})
|
|
|
|
if (!res.ok) return ''
|
|
|
|
const data = await res.json()
|
|
if (data.results?.length > 0) {
|
|
return data.results
|
|
.map(
|
|
(r: { source_name?: string; source_code?: string; content?: string }, i: number) =>
|
|
`[Quelle ${i + 1}: ${r.source_name || r.source_code || 'Unbekannt'}]\n${r.content || ''}`
|
|
)
|
|
.join('\n\n---\n\n')
|
|
}
|
|
return ''
|
|
} catch {
|
|
return ''
|
|
}
|
|
}
|
|
|
|
export async function POST(request: NextRequest) {
|
|
try {
|
|
const body = await request.json()
|
|
const {
|
|
message,
|
|
history = [],
|
|
sdkStateProjection,
|
|
mode = 'explain',
|
|
documentType,
|
|
} = body
|
|
|
|
if (!message || typeof message !== 'string') {
|
|
return NextResponse.json({ error: 'Message is required' }, { status: 400 })
|
|
}
|
|
|
|
// 1. Query RAG for legal context
|
|
const ragContext = await queryRAG(message)
|
|
|
|
// 2. Build system prompt with mode-specific instructions + state projection
|
|
let systemContent = DRAFTING_SYSTEM_PROMPT
|
|
|
|
// Mode-specific instructions
|
|
const modeInstructions: Record<string, string> = {
|
|
explain: '\n\n## Aktueller Modus: EXPLAIN\nBeantworte Fragen verstaendlich mit Quellenangaben.',
|
|
ask: '\n\n## Aktueller Modus: ASK\nAnalysiere Luecken und stelle gezielte Fragen. Eine Frage pro Antwort.',
|
|
draft: `\n\n## Aktueller Modus: DRAFT\nEntwirf strukturierte Dokument-Sections. Dokumenttyp: ${documentType || 'nicht spezifiziert'}.\nAntworte mit JSON wenn ein Draft angefragt wird.`,
|
|
validate: '\n\n## Aktueller Modus: VALIDATE\nPruefe Cross-Dokument-Konsistenz. Gib Errors, Warnings und Suggestions zurueck.',
|
|
}
|
|
systemContent += modeInstructions[mode] || modeInstructions.explain
|
|
|
|
// Add state projection context
|
|
if (sdkStateProjection) {
|
|
systemContent += `\n\n## SDK-State Projektion (${mode}-Kontext)\n${JSON.stringify(sdkStateProjection, null, 0).slice(0, 3000)}`
|
|
}
|
|
|
|
// Add RAG context
|
|
if (ragContext) {
|
|
systemContent += `\n\n## Relevanter Rechtskontext\n${ragContext}`
|
|
}
|
|
|
|
// 3. Build messages array
|
|
const messages = [
|
|
{ role: 'system', content: systemContent },
|
|
...history.slice(-10).map((h: { role: string; content: string }) => ({
|
|
role: h.role === 'user' ? 'user' : 'assistant',
|
|
content: h.content,
|
|
})),
|
|
{ role: 'user', content: message },
|
|
]
|
|
|
|
// 4. Call LLM with streaming
|
|
const ollamaResponse = await fetch(`${OLLAMA_URL}/api/chat`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
model: LLM_MODEL,
|
|
messages,
|
|
stream: true,
|
|
options: {
|
|
temperature: mode === 'draft' ? 0.2 : 0.3,
|
|
num_predict: mode === 'draft' ? 16384 : 8192,
|
|
},
|
|
}),
|
|
signal: AbortSignal.timeout(120000),
|
|
})
|
|
|
|
if (!ollamaResponse.ok) {
|
|
const errorText = await ollamaResponse.text()
|
|
console.error('LLM error:', ollamaResponse.status, errorText)
|
|
return NextResponse.json(
|
|
{ error: `LLM nicht erreichbar (Status ${ollamaResponse.status})` },
|
|
{ status: 502 }
|
|
)
|
|
}
|
|
|
|
// 5. Stream response back
|
|
const encoder = new TextEncoder()
|
|
const stream = new ReadableStream({
|
|
async start(controller) {
|
|
const reader = ollamaResponse.body!.getReader()
|
|
const decoder = new TextDecoder()
|
|
|
|
try {
|
|
while (true) {
|
|
const { done, value } = await reader.read()
|
|
if (done) break
|
|
|
|
const chunk = decoder.decode(value, { stream: true })
|
|
const lines = chunk.split('\n').filter((l) => l.trim())
|
|
|
|
for (const line of lines) {
|
|
try {
|
|
const json = JSON.parse(line)
|
|
if (json.message?.content) {
|
|
controller.enqueue(encoder.encode(json.message.content))
|
|
}
|
|
} catch {
|
|
// Partial JSON, skip
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Stream error:', error)
|
|
} finally {
|
|
controller.close()
|
|
}
|
|
},
|
|
})
|
|
|
|
return new NextResponse(stream, {
|
|
headers: {
|
|
'Content-Type': 'text/plain; charset=utf-8',
|
|
'Cache-Control': 'no-cache',
|
|
'Connection': 'keep-alive',
|
|
},
|
|
})
|
|
} catch (error) {
|
|
console.error('Drafting engine chat error:', error)
|
|
return NextResponse.json(
|
|
{ error: 'Verbindung zum LLM fehlgeschlagen.' },
|
|
{ status: 503 }
|
|
)
|
|
}
|
|
}
|