Addressed test results from Test_status_Wkf.md: **Issue #5: End-Node Überschriften** - Fixed aggregate_results to show node labels instead of "Node 10" - Added graph lookup to get node.data.label from node objects - Modified backend/workflow_executor.py (2 locations) **Issue #8: Löschen-Taste funktioniert nicht** - Added Delete key support to WorkflowCanvas - Set deleteKeyCode={['Backspace', 'Delete']} - Frontend: WorkflowCanvas.jsx **Issue #9: Mehrere End-Nodes verhindern** - Added validation error when multiple End-Nodes exist - Backend supports only 1 End-Node (aggregate_results takes last) - Frontend: workflowValidation.js **Issue #11: Export Fehler "Internal Server Error"** - Added missing fields to export-all endpoint: - graph_data (workflow node graph) - question_augmentations (analysis prompts) - Added missing fields to import endpoint - Proper JSON serialization for all JSONB fields - Backend: routers/prompts.py **Issue #12: Workflow duplizieren funktioniert nicht** - Fixed duplicate endpoint to include all prompt fields: - type, stages, output_format, output_schema - question_augmentations, graph_data (critical for workflows!) - Backend: routers/prompts.py Files changed: - backend/workflow_executor.py: Node label lookup in aggregate_results - backend/routers/prompts.py: Export/import/duplicate fixes - frontend/src/components/workflow/WorkflowCanvas.jsx: Delete key - frontend/src/utils/workflowValidation.js: Max 1 End-Node validation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
247 lines
6.6 KiB
JavaScript
247 lines
6.6 KiB
JavaScript
/**
|
|
* Workflow Validation Utilities
|
|
*
|
|
* Validiert Workflow-Graphen (Struktur + Logik).
|
|
*/
|
|
|
|
export function validateWorkflowGraph(nodes, edges) {
|
|
const errors = []
|
|
const warnings = []
|
|
|
|
// 1. Strukturelle Validierung
|
|
validateStructure(nodes, edges, errors, warnings)
|
|
|
|
// 2. Logische Validierung
|
|
validateLogic(nodes, edges, errors, warnings)
|
|
|
|
return {
|
|
errors,
|
|
warnings,
|
|
isValid: errors.length === 0
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Strukturelle Validierung (DAG, START/END, Zyklen)
|
|
*/
|
|
function validateStructure(nodes, edges, errors, warnings) {
|
|
// START Node
|
|
const startNodes = nodes.filter(n => n.type === 'start')
|
|
if (startNodes.length === 0) {
|
|
errors.push({
|
|
type: 'structure',
|
|
message: 'Kein START-Node vorhanden',
|
|
severity: 'error'
|
|
})
|
|
} else if (startNodes.length > 1) {
|
|
errors.push({
|
|
type: 'structure',
|
|
message: `${startNodes.length} START-Nodes gefunden (max. 1 erlaubt)`,
|
|
severity: 'error'
|
|
})
|
|
}
|
|
|
|
// END Node
|
|
const endNodes = nodes.filter(n => n.type === 'end')
|
|
if (endNodes.length === 0) {
|
|
errors.push({
|
|
type: 'structure',
|
|
message: 'Kein END-Node vorhanden',
|
|
severity: 'error'
|
|
})
|
|
} else if (endNodes.length > 1) {
|
|
// Backend unterstützt aktuell nur 1 End-Node (aggregate_results nimmt letzten)
|
|
// Future: Multi-Exit Support würde Backend-Anpassung erfordern
|
|
errors.push({
|
|
type: 'structure',
|
|
message: `${endNodes.length} END-Nodes gefunden (max. 1 erlaubt)`,
|
|
severity: 'error'
|
|
})
|
|
}
|
|
|
|
// Zyklen-Erkennung
|
|
if (detectCycles(nodes, edges)) {
|
|
errors.push({
|
|
type: 'structure',
|
|
message: 'Workflow enthält Zyklen (nicht erlaubt)',
|
|
severity: 'error'
|
|
})
|
|
}
|
|
|
|
// Isolierte Nodes
|
|
nodes.forEach(node => {
|
|
if (node.type === 'start' || node.type === 'end') return
|
|
|
|
const hasIncoming = edges.some(e => e.target === node.id)
|
|
const hasOutgoing = edges.some(e => e.source === node.id)
|
|
|
|
if (!hasIncoming || !hasOutgoing) {
|
|
warnings.push({
|
|
type: 'isolation',
|
|
message: `Node "${node.data.label}" ist isoliert (keine/fehlende Verbindungen)`,
|
|
nodeId: node.id,
|
|
severity: 'warning'
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Logische Validierung (Node-Konfiguration)
|
|
*/
|
|
function validateLogic(nodes, edges, errors, warnings) {
|
|
nodes.forEach(node => {
|
|
// Analysis Nodes
|
|
if (node.type === 'analysis') {
|
|
const questions = node.data.questions || []
|
|
const hasPromptSlug = node.data.prompt_slug != null && node.data.prompt_slug !== ''
|
|
const hasInlineTemplate = node.data.inline_template != null && node.data.inline_template.trim() !== ''
|
|
|
|
// Part 3: Validation - Entweder prompt_slug ODER inline_template
|
|
if (!hasPromptSlug && !hasInlineTemplate) {
|
|
errors.push({
|
|
type: 'config',
|
|
message: `Analysis-Node "${node.data.label}" benötigt entweder Basis-Prompt oder Inline-Template`,
|
|
nodeId: node.id,
|
|
severity: 'error'
|
|
})
|
|
}
|
|
|
|
// Warning wenn beide gesetzt (sollte nicht passieren, aber zur Sicherheit)
|
|
if (hasPromptSlug && hasInlineTemplate) {
|
|
warnings.push({
|
|
type: 'config',
|
|
message: `Analysis-Node "${node.data.label}" hat sowohl Basis-Prompt als auch Inline-Template - Inline hat Vorrang`,
|
|
nodeId: node.id,
|
|
severity: 'warning'
|
|
})
|
|
}
|
|
|
|
// Fragen validieren
|
|
questions.forEach((q, idx) => {
|
|
if (!q.question?.trim()) {
|
|
errors.push({
|
|
type: 'config',
|
|
message: `Frage ${idx + 1} in "${node.data.label}" hat keinen Text`,
|
|
nodeId: node.id,
|
|
severity: 'error'
|
|
})
|
|
}
|
|
|
|
if (!q.answer_spectrum || q.answer_spectrum.length < 2) {
|
|
errors.push({
|
|
type: 'config',
|
|
message: `Frage ${idx + 1} in "${node.data.label}" braucht mind. 2 Antworten`,
|
|
nodeId: node.id,
|
|
severity: 'error'
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
// Logic Nodes
|
|
if (node.type === 'logic') {
|
|
const condition = node.data.condition
|
|
|
|
if (!condition || !condition.operator) {
|
|
errors.push({
|
|
type: 'config',
|
|
message: `Logic-Node "${node.data.label}" hat keine Bedingung`,
|
|
nodeId: node.id,
|
|
severity: 'error'
|
|
})
|
|
} else {
|
|
// Bedingung vollständig?
|
|
const incomplete = findIncompleteConditions(condition)
|
|
if (incomplete.length > 0) {
|
|
errors.push({
|
|
type: 'config',
|
|
message: `Logic-Node "${node.data.label}" hat ${incomplete.length} unvollständige Bedingung(en)`,
|
|
nodeId: node.id,
|
|
severity: 'error'
|
|
})
|
|
}
|
|
}
|
|
|
|
// Mind. 2 Outgoing Edges (true/false Pfade)
|
|
const outgoing = edges.filter(e => e.source === node.id)
|
|
if (outgoing.length < 2) {
|
|
warnings.push({
|
|
type: 'config',
|
|
message: `Logic-Node "${node.data.label}" hat nur ${outgoing.length} Ausgang (sollte mind. 2 haben)`,
|
|
nodeId: node.id,
|
|
severity: 'warning'
|
|
})
|
|
}
|
|
}
|
|
|
|
// Join Nodes
|
|
if (node.type === 'join') {
|
|
const incoming = edges.filter(e => e.target === node.id)
|
|
|
|
if (incoming.length < 2) {
|
|
warnings.push({
|
|
type: 'config',
|
|
message: `Join-Node "${node.data.label}" hat nur ${incoming.length} eingehende Kante (sollte mind. 2 haben)`,
|
|
nodeId: node.id,
|
|
severity: 'warning'
|
|
})
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Zyklen-Erkennung (DFS-basiert)
|
|
*/
|
|
function detectCycles(nodes, edges) {
|
|
const visited = new Set()
|
|
const recStack = new Set()
|
|
|
|
function dfs(nodeId) {
|
|
visited.add(nodeId)
|
|
recStack.add(nodeId)
|
|
|
|
const outgoing = edges.filter(e => e.source === nodeId)
|
|
for (const edge of outgoing) {
|
|
if (!visited.has(edge.target)) {
|
|
if (dfs(edge.target)) return true
|
|
} else if (recStack.has(edge.target)) {
|
|
return true // Cycle detected
|
|
}
|
|
}
|
|
|
|
recStack.delete(nodeId)
|
|
return false
|
|
}
|
|
|
|
for (const node of nodes) {
|
|
if (!visited.has(node.id)) {
|
|
if (dfs(node.id)) return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* Unvollständige Bedingungen finden (rekursiv)
|
|
*/
|
|
function findIncompleteConditions(condition) {
|
|
const incomplete = []
|
|
|
|
// Verschachtelte Gruppe?
|
|
if (condition.operands && Array.isArray(condition.operands)) {
|
|
for (const op of condition.operands) {
|
|
incomplete.push(...findIncompleteConditions(op))
|
|
}
|
|
} else {
|
|
// Einfache Bedingung: ref, operator, value müssen gesetzt sein
|
|
if (!condition.ref || !condition.operator || condition.value === undefined || condition.value === '') {
|
|
incomplete.push(condition)
|
|
}
|
|
}
|
|
|
|
return incomplete
|
|
}
|