feat(sdk): Add Drafting Engine with 4-mode agent system (Explain/Ask/Draft/Validate)

Extends the Compliance Advisor from a Q&A chatbot into a full drafting engine
that can generate, validate, and refine compliance documents within Scope Engine
constraints. Includes intent classifier, state projector, constraint enforcer,
SOUL templates, Go backend endpoints, and React UI components.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
BreakPilot Dev
2026-02-11 12:37:18 +01:00
parent f927c0c205
commit 206183670d
26 changed files with 4639 additions and 1 deletions

View File

@@ -0,0 +1,343 @@
'use client'
/**
* useDraftingEngine - React Hook fuer die Drafting Engine
*
* Managed: currentMode, activeDocumentType, draftSessions, validationState
* Handled: State-Projection, API-Calls, Streaming
* Provides: sendMessage(), requestDraft(), validateDraft(), acceptDraft()
*/
import { useState, useCallback, useRef } from 'react'
import { useSDK } from '../context'
import { stateProjector } from './state-projector'
import { intentClassifier } from './intent-classifier'
import { constraintEnforcer } from './constraint-enforcer'
import type {
AgentMode,
DraftSession,
DraftRevision,
DraftingChatMessage,
ValidationResult,
ConstraintCheckResult,
DraftContext,
GapContext,
ValidationContext,
} from './types'
import type { ScopeDocumentType } from '../compliance-scope-types'
export interface DraftingEngineState {
currentMode: AgentMode
activeDocumentType: ScopeDocumentType | null
messages: DraftingChatMessage[]
isTyping: boolean
currentDraft: DraftRevision | null
validationResult: ValidationResult | null
constraintCheck: ConstraintCheckResult | null
error: string | null
}
export interface DraftingEngineActions {
setMode: (mode: AgentMode) => void
setDocumentType: (type: ScopeDocumentType) => void
sendMessage: (content: string) => Promise<void>
requestDraft: (instructions?: string) => Promise<void>
validateDraft: () => Promise<void>
acceptDraft: () => void
stopGeneration: () => void
clearMessages: () => void
}
export function useDraftingEngine(): DraftingEngineState & DraftingEngineActions {
const { state, dispatch } = useSDK()
const abortControllerRef = useRef<AbortController | null>(null)
const [currentMode, setCurrentMode] = useState<AgentMode>('explain')
const [activeDocumentType, setActiveDocumentType] = useState<ScopeDocumentType | null>(null)
const [messages, setMessages] = useState<DraftingChatMessage[]>([])
const [isTyping, setIsTyping] = useState(false)
const [currentDraft, setCurrentDraft] = useState<DraftRevision | null>(null)
const [validationResult, setValidationResult] = useState<ValidationResult | null>(null)
const [constraintCheck, setConstraintCheck] = useState<ConstraintCheckResult | null>(null)
const [error, setError] = useState<string | null>(null)
// Get state projection based on mode
const getProjection = useCallback(() => {
switch (currentMode) {
case 'draft':
return activeDocumentType
? stateProjector.projectForDraft(state, activeDocumentType)
: null
case 'ask':
return stateProjector.projectForAsk(state)
case 'validate':
return activeDocumentType
? stateProjector.projectForValidate(state, [activeDocumentType])
: stateProjector.projectForValidate(state, ['vvt', 'tom', 'lf'])
default:
return activeDocumentType
? stateProjector.projectForDraft(state, activeDocumentType)
: null
}
}, [state, currentMode, activeDocumentType])
const setMode = useCallback((mode: AgentMode) => {
setCurrentMode(mode)
}, [])
const setDocumentType = useCallback((type: ScopeDocumentType) => {
setActiveDocumentType(type)
}, [])
const sendMessage = useCallback(async (content: string) => {
if (!content.trim() || isTyping) return
setError(null)
// Auto-detect mode if needed
const classification = intentClassifier.classify(content)
if (classification.confidence > 0.7 && classification.mode !== currentMode) {
setCurrentMode(classification.mode)
}
if (classification.detectedDocumentType && !activeDocumentType) {
setActiveDocumentType(classification.detectedDocumentType)
}
const userMessage: DraftingChatMessage = {
role: 'user',
content: content.trim(),
}
setMessages(prev => [...prev, userMessage])
setIsTyping(true)
abortControllerRef.current = new AbortController()
try {
const projection = getProjection()
const response = await fetch('/api/sdk/drafting-engine/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: content.trim(),
history: messages.map(m => ({ role: m.role, content: m.content })),
sdkStateProjection: projection,
mode: currentMode,
documentType: activeDocumentType,
}),
signal: abortControllerRef.current.signal,
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: 'Unbekannter Fehler' }))
throw new Error(errorData.error || `Server-Fehler (${response.status})`)
}
const agentMessageId = `msg-${Date.now()}-agent`
setMessages(prev => [...prev, {
role: 'assistant',
content: '',
metadata: { mode: currentMode, documentType: activeDocumentType ?? undefined },
}])
// Stream response
const reader = response.body!.getReader()
const decoder = new TextDecoder()
let accumulated = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
accumulated += decoder.decode(value, { stream: true })
const text = accumulated
setMessages(prev =>
prev.map((m, i) => i === prev.length - 1 ? { ...m, content: text } : m)
)
}
setIsTyping(false)
} catch (err) {
if ((err as Error).name === 'AbortError') {
setIsTyping(false)
return
}
setError((err as Error).message)
setMessages(prev => [...prev, {
role: 'assistant',
content: `Fehler: ${(err as Error).message}`,
}])
setIsTyping(false)
}
}, [isTyping, messages, currentMode, activeDocumentType, getProjection])
const requestDraft = useCallback(async (instructions?: string) => {
if (!activeDocumentType) {
setError('Bitte waehlen Sie zuerst einen Dokumenttyp.')
return
}
setError(null)
setIsTyping(true)
try {
const draftContext = stateProjector.projectForDraft(state, activeDocumentType)
const response = await fetch('/api/sdk/drafting-engine/draft', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
documentType: activeDocumentType,
draftContext,
instructions,
existingDraft: currentDraft,
}),
})
const result = await response.json()
if (!response.ok) {
throw new Error(result.error || 'Draft-Generierung fehlgeschlagen')
}
setCurrentDraft(result.draft)
setConstraintCheck(result.constraintCheck)
setMessages(prev => [...prev, {
role: 'assistant',
content: `Draft fuer ${activeDocumentType} erstellt (${result.draft.sections.length} Sections). Oeffnen Sie den Editor zur Bearbeitung.`,
metadata: { mode: 'draft', documentType: activeDocumentType, hasDraft: true },
}])
setIsTyping(false)
} catch (err) {
setError((err as Error).message)
setIsTyping(false)
}
}, [activeDocumentType, state, currentDraft])
const validateDraft = useCallback(async () => {
setError(null)
setIsTyping(true)
try {
const docTypes: ScopeDocumentType[] = activeDocumentType
? [activeDocumentType]
: ['vvt', 'tom', 'lf']
const validationContext = stateProjector.projectForValidate(state, docTypes)
const response = await fetch('/api/sdk/drafting-engine/validate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
documentType: activeDocumentType || 'vvt',
draftContent: currentDraft?.content || '',
validationContext,
}),
})
const result = await response.json()
if (!response.ok) {
throw new Error(result.error || 'Validierung fehlgeschlagen')
}
setValidationResult(result)
const summary = result.passed
? `Validierung bestanden. ${result.warnings.length} Warnungen, ${result.suggestions.length} Vorschlaege.`
: `Validierung fehlgeschlagen. ${result.errors.length} Fehler, ${result.warnings.length} Warnungen.`
setMessages(prev => [...prev, {
role: 'assistant',
content: summary,
metadata: { mode: 'validate', hasValidation: true },
}])
setIsTyping(false)
} catch (err) {
setError((err as Error).message)
setIsTyping(false)
}
}, [activeDocumentType, state, currentDraft])
const acceptDraft = useCallback(() => {
if (!currentDraft || !activeDocumentType) return
// Dispatch the draft data into SDK state
switch (activeDocumentType) {
case 'vvt':
dispatch({
type: 'ADD_PROCESSING_ACTIVITY',
payload: {
id: `draft-vvt-${Date.now()}`,
name: currentDraft.sections.find(s => s.schemaField === 'name')?.content || 'Neuer VVT-Eintrag',
...Object.fromEntries(
currentDraft.sections
.filter(s => s.schemaField)
.map(s => [s.schemaField!, s.content])
),
},
})
break
case 'tom':
dispatch({
type: 'ADD_TOM',
payload: {
id: `draft-tom-${Date.now()}`,
name: 'TOM-Entwurf',
...Object.fromEntries(
currentDraft.sections
.filter(s => s.schemaField)
.map(s => [s.schemaField!, s.content])
),
},
})
break
default:
dispatch({
type: 'ADD_DOCUMENT',
payload: {
id: `draft-${activeDocumentType}-${Date.now()}`,
type: activeDocumentType,
content: currentDraft.content,
sections: currentDraft.sections,
},
})
}
setMessages(prev => [...prev, {
role: 'assistant',
content: `Draft wurde in den SDK-State uebernommen.`,
}])
setCurrentDraft(null)
}, [currentDraft, activeDocumentType, dispatch])
const stopGeneration = useCallback(() => {
abortControllerRef.current?.abort()
setIsTyping(false)
}, [])
const clearMessages = useCallback(() => {
setMessages([])
setCurrentDraft(null)
setValidationResult(null)
setConstraintCheck(null)
setError(null)
}, [])
return {
currentMode,
activeDocumentType,
messages,
isTyping,
currentDraft,
validationResult,
constraintCheck,
error,
setMode,
setDocumentType,
sendMessage,
requestDraft,
validateDraft,
acceptDraft,
stopGeneration,
clearMessages,
}
}