mitai-jinkendo/frontend/src/utils/workflowValidation.js
Lars a1723db387
Some checks failed
Deploy Development / deploy (push) Successful in 56s
Build Test / pytest-backend (push) Failing after 4s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 17s
feat: Workflow Engine Part 3 - Inline Prompts (v0.9q)
Ermöglicht Analysis Nodes zwischen zwei Prompt-Modi zu wählen:
- Reference Mode: Basis-Prompt aus DB referenzieren (bestehend)
- Inline Mode: Template direkt im Node editieren (NEU)

Frontend:
- InlineTemplateEditor Component (~80 Zeilen)
- Radio Buttons in WorkflowEditorPage für Mode-Auswahl
- Placeholder Picker für beide Modi (End Node + Inline Template)
- Cursor-Position Tracking mit textareaRef
- Conditional Rendering basierend auf promptSource
- Validation: Entweder prompt_slug ODER inline_template

Backend:
- load_prompt_template() akzeptiert ganzen WorkflowNode (statt nur slug)
- Unterstützt inline_template (Mode 1) und prompt_slug (Mode 2)
- WorkflowNode.inline_template Feld hinzugefügt
- Validation: HTTPException wenn weder slug noch template

Serialization:
- inline_template in graph_data speichern/laden
- Backward-compatible mit bestehenden Workflows

Version: 0.9q
Module: workflow 0.7.0

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-11 08:45:00 +02:00

239 lines
6.3 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'
})
}
// 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
}