diff --git a/backend/workflow_executor.py b/backend/workflow_executor.py index 6e36ad4..1f12fba 100644 --- a/backend/workflow_executor.py +++ b/backend/workflow_executor.py @@ -166,13 +166,19 @@ async def execute_workflow( enable_debug=enable_debug ) + # Add human-readable label to node_state for debug UI + # Priority: node.label (user-defined) > prompt_slug > node_type-id + node_label = node.label if hasattr(node, 'label') and node.label else ( + node.prompt_slug if hasattr(node, 'prompt_slug') and node.prompt_slug else f"{node.type.value}-{node_id[:8]}" + ) + if not node_state.debug_prompt_slug: + node_state.debug_prompt_slug = node_label + node_states.append(node_state) context["node_results"][node_id] = node_state # NEW: Progress-Callback aufrufen (für SSE Streaming) if progress_callback: - # Create a meaningful label for the node - node_label = node.prompt_slug if hasattr(node, 'prompt_slug') and node.prompt_slug else f"{node.type.value}-{node_id[:8]}" await progress_callback("node_complete", { "node_id": node_id, "node_type": node.type, @@ -314,11 +320,11 @@ async def execute_node( # Logic Nodes (Phase 3) if node.type == "logic": - return execute_logic_node(node, context, graph) + return execute_logic_node(node, context, graph, enable_debug) # Join Nodes (Phase 4) if node.type == "join": - return execute_join_node(node, context, graph) + return execute_join_node(node, context, graph, enable_debug) # Analysis Nodes if node.type == "analysis": @@ -398,6 +404,11 @@ async def execute_node( decision_signals=parsed["decision_signals"], normalized_signals=normalized_signals, reasoning_anchors=parsed.get("reasoning_anchors"), + # Debug information (nur wenn enable_debug=True) + debug_prompt=augmented_prompt if enable_debug else None, + debug_raw_response=llm_response if enable_debug else None, + debug_node_type="analysis", + debug_prompt_slug=node.prompt_slug if enable_debug else None, started_at=started_at, completed_at=datetime.utcnow().isoformat() ) @@ -419,7 +430,8 @@ async def execute_node( def execute_logic_node( node, context: Dict[str, Any], - graph: WorkflowGraph + graph: WorkflowGraph, + enable_debug: bool = False ) -> NodeExecutionState: """ Führt Logic Node aus (Phase 3). @@ -519,7 +531,15 @@ def execute_logic_node( "evaluation_result": result if not error else None, "error": error, "activated_edges": activated_edges - }) + }), + # Debug information + debug_prompt=json.dumps(expression.model_dump() if hasattr(expression, 'model_dump') else str(expression), ensure_ascii=False) if enable_debug and expression else None, + debug_raw_response=json.dumps({ + "result": result, + "error": error, + "activated_edges": activated_edges + }, ensure_ascii=False) if enable_debug else None, + debug_node_type="logic" ) except Exception as e: @@ -536,7 +556,8 @@ def execute_logic_node( def execute_join_node( node, context: Dict[str, Any], - graph: WorkflowGraph + graph: WorkflowGraph, + enable_debug: bool = False ) -> NodeExecutionState: """ Führt Join Node aus (Phase 4). @@ -608,7 +629,16 @@ def execute_join_node( normalized_signals=consolidated_signals_list, metadata=join_result.metadata, started_at=started_at, - completed_at=datetime.utcnow().isoformat() + completed_at=datetime.utcnow().isoformat(), + # Debug information + debug_prompt=f"Join Strategy: {node.join_strategy.value if node.join_strategy else 'wait_all'}\nPaths: {total_count}\nMinimum Required: {node.min_paths if node.min_paths else 'all'}" if enable_debug else None, + debug_raw_response=json.dumps({ + "executed_paths": executed_count, + "total_paths": total_count, + "strategy": node.join_strategy.value if node.join_strategy else 'wait_all', + "consolidated_nodes": list(join_result.consolidated_analysis_core.keys()) + }, ensure_ascii=False) if enable_debug else None, + debug_node_type="join" ) except Exception as e: diff --git a/backend/workflow_models.py b/backend/workflow_models.py index 0467285..5f2f98e 100644 --- a/backend/workflow_models.py +++ b/backend/workflow_models.py @@ -196,6 +196,7 @@ class WorkflowNode(BaseModel): """ id: str = Field(..., description="Eindeutige Knoten-ID") type: NodeType = Field(..., description="Knotentyp") + label: Optional[str] = Field(None, description="Node-Label (vom Editor, z.B. 'Qualitätseinschätzung')") position: Optional[Position] = Field(None, description="Position im visuellen Editor") # ANALYSIS-Knoten @@ -349,6 +350,15 @@ class NodeExecutionState(BaseModel): normalized_signals: List[NormalizedSignal] = Field(default_factory=list, description="Normalisierte Signale (Phase 2)") reasoning_anchors: Optional[str] = Field(None, description="Begründungsanker aus ## Begründung") + # Debug Information (nur wenn enable_debug=True) + debug_prompt: Optional[str] = Field(None, description="Vollständiger Prompt der an die KI gesendet wurde") + debug_raw_response: Optional[str] = Field(None, description="Rohe KI-Antwort (ungeparst)") + debug_node_type: Optional[str] = Field(None, description="Node-Typ (analysis, logic, join, etc.)") + debug_prompt_slug: Optional[str] = Field(None, description="Verwendeter Prompt-Slug (bei ANALYSIS nodes)") + + # Metadata (für Join Nodes und andere Zusatzinfos) + metadata: Optional[Dict[str, Any]] = Field(None, description="Zusätzliche Metadaten (z.B. Join-Statistiken)") + # Error & Timing error: Optional[str] = Field(None, description="Fehlermeldung bei failed") started_at: Optional[str] = Field(None, description="Start-Timestamp (ISO)") diff --git a/frontend/src/components/WorkflowDebugPanel.jsx b/frontend/src/components/WorkflowDebugPanel.jsx new file mode 100644 index 0000000..7f80c42 --- /dev/null +++ b/frontend/src/components/WorkflowDebugPanel.jsx @@ -0,0 +1,356 @@ +import React, { useState } from 'react' +import { ChevronDown, ChevronUp } from 'lucide-react' + +/** + * WorkflowDebugPanel - Zeigt Debug-Informationen für jeden Node eines Workflows + * + * @param {Object} props + * @param {Array} props.nodeStates - Array von NodeExecutionState Objekten + */ +export default function WorkflowDebugPanel({ nodeStates }) { + const [expandedNodes, setExpandedNodes] = useState(new Set()) + const [showAll, setShowAll] = useState(false) + + if (!nodeStates || nodeStates.length === 0) { + return null + } + + // Filter nodes that have debug information + const debugNodes = nodeStates.filter(ns => + ns.debug_prompt || ns.debug_raw_response || ns.debug_node_type + ) + + if (debugNodes.length === 0) { + return ( +
+

+ Keine Debug-Informationen verfügbar. Führe den Workflow mit debug=true aus. +

+
+ ) + } + + const toggleNode = (nodeId) => { + const newExpanded = new Set(expandedNodes) + if (newExpanded.has(nodeId)) { + newExpanded.delete(nodeId) + } else { + newExpanded.add(nodeId) + } + setExpandedNodes(newExpanded) + } + + const toggleAll = () => { + if (showAll) { + setExpandedNodes(new Set()) + } else { + setExpandedNodes(new Set(debugNodes.map(n => n.node_id))) + } + setShowAll(!showAll) + } + + const getStatusColor = (status) => { + switch (status) { + case 'executed': return '#1D9E75' + case 'failed': return '#D85A30' + case 'skipped': return '#888' + default: return 'var(--text2)' + } + } + + const getNodeLabel = (node) => { + if (node.debug_prompt_slug) return node.debug_prompt_slug + if (node.debug_node_type) return `${node.debug_node_type}-${node.node_id.substring(0, 8)}` + return node.node_id + } + + return ( +
+
+

+ 🔍 Debug-Informationen ({debugNodes.length} Nodes) +

+ +
+ +
+ {debugNodes.map((node, idx) => { + const isExpanded = expandedNodes.has(node.node_id) + const label = getNodeLabel(node) + const statusColor = getStatusColor(node.status) + + const hasFailed = node.status === 'failed' + const hasError = node.error != null + + return ( +
+ {/* Node Header */} +
toggleNode(node.node_id)} + style={{ + padding: '12px', + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + cursor: 'pointer', + background: isExpanded ? 'var(--surface)' : 'transparent', + transition: 'background 0.2s' + }} + > +
+ + #{idx + 1} + + + {label} + + + {node.status} + + {node.debug_node_type && ( + + {node.debug_node_type} + + )} +
+ {isExpanded ? : } +
+ + {/* Node Debug Content */} + {isExpanded && ( +
+ {node.error && ( +
+
+ Error: +
+
+                        {node.error}
+                      
+
+ )} + + {node.debug_prompt && ( +
+
+ Prompt: +
+
+                        {node.debug_prompt}
+                      
+
+ )} + + {node.debug_raw_response && ( +
+
+ Rohe Antwort: +
+
+                        {node.debug_raw_response}
+                      
+
+ )} + + {node.analysis_core && ( +
+
+ Geparste Ergebnisse: +
+
+                        {node.analysis_core}
+                      
+
+ )} + + {node.normalized_signals && node.normalized_signals.length > 0 && ( +
+
+ Signale ({node.normalized_signals.length}): +
+
+ {node.normalized_signals.map((sig, i) => ( +
+ {sig.question_type}:{' '} + + "{sig.raw_value}" → "{sig.normalized_value}" + {' '} + + {sig.status} + +
+ ))} +
+
+ )} +
+ )} +
+ ) + })} +
+
+ ) +} diff --git a/frontend/src/components/workflow/panels/WorkflowResultViewer.jsx b/frontend/src/components/workflow/panels/WorkflowResultViewer.jsx index 6a596a5..9971180 100644 --- a/frontend/src/components/workflow/panels/WorkflowResultViewer.jsx +++ b/frontend/src/components/workflow/panels/WorkflowResultViewer.jsx @@ -192,13 +192,17 @@ export function WorkflowResultViewer({ result, onClose }) { Node States (Debug)
- {nodeStates.map((node) => ( + {nodeStates.map((node) => { + const hasFailed = node.status === 'failed' + return (
{/* Node Header */} @@ -216,13 +220,13 @@ export function WorkflowResultViewer({ result, onClose }) { }} >
- {node.node_type === 'start' && '🚀'} - {node.node_type === 'analysis' && '🤖'} - {node.node_type === 'logic' && '⚡'} - {node.node_type === 'join' && '🔀'} - {node.node_type === 'end' && '🏁'} + {(node.debug_node_type || node.node_type) === 'start' && '🚀'} + {(node.debug_node_type || node.node_type) === 'analysis' && '🤖'} + {(node.debug_node_type || node.node_type) === 'logic' && '⚡'} + {(node.debug_node_type || node.node_type) === 'join' && '🔀'} + {(node.debug_node_type || node.node_type) === 'end' && '🏁'} {' '} - {node.node_label || node.node_id} + {node.debug_prompt_slug || node.node_label || ((node.debug_node_type || node.node_type) ? `${node.debug_node_type || node.node_type}-${node.node_id.substring(0, 8)}` : node.node_id)} {node.status === 'skipped' && ( (skipped) @@ -237,45 +241,65 @@ export function WorkflowResultViewer({ result, onClose }) { {/* Node Details */} {expandedNodes[node.node_id] && (
- {node.output && ( + {/* Error (show first) */} + {node.error && ( +
+
Error:
+
{node.error}
+
+ )} + + {/* Debug Prompt */} + {node.debug_prompt && ( +
+
Prompt:
+
{node.debug_prompt}
+
+ )} + + {/* Debug Raw Response */} + {node.debug_raw_response && ( +
+
Rohe Antwort:
+
{node.debug_raw_response}
+
+ )} + + {/* Analysis Core */} + {node.analysis_core && ( +
+
Ergebnis:
+
{node.analysis_core}
+
+ )} + + {/* Normalized Signals */} + {node.normalized_signals && node.normalized_signals.length > 0 && ( +
+
Signale ({node.normalized_signals.length}):
+ {node.normalized_signals.map((sig, i) => ( +
+ {sig.question_type}: "{sig.raw_value}" → "{sig.normalized_value}" {sig.status} +
+ ))} +
+ )} + + {/* Output (fallback) */} + {node.output && !node.analysis_core && (
Output: -
-                            {typeof node.output === 'string'
-                              ? node.output
-                              : JSON.stringify(node.output, null, 2)}
+                          
+                            {typeof node.output === 'string' ? node.output : JSON.stringify(node.output, null, 2)}
                           
)} - {node.error && ( -
- Error: {node.error} -
- )} + + {/* Metadata */} {node.metadata && (
Metadata: -
+                          
                             {JSON.stringify(node.metadata, null, 2)}
                           
@@ -283,7 +307,7 @@ export function WorkflowResultViewer({ result, onClose }) {
)}
- ))} + )})}
)} diff --git a/frontend/src/pages/Analysis.jsx b/frontend/src/pages/Analysis.jsx index 8f03c81..8676417 100644 --- a/frontend/src/pages/Analysis.jsx +++ b/frontend/src/pages/Analysis.jsx @@ -10,6 +10,7 @@ import { import { useAuth } from '../context/AuthContext' import Markdown from '../utils/Markdown' import UsageBadge from '../components/UsageBadge' +import WorkflowDebugPanel from '../components/WorkflowDebugPanel' import dayjs from 'dayjs' import 'dayjs/locale/de' dayjs.locale('de') @@ -382,7 +383,7 @@ export default function Analysis() { setLoading(slug); setError(null); setNewResult(null); setProgress(null) try { // Use SSE-based executor for long-running workflows - const result = await api.executeUnifiedPromptStream(slug, null, null, false, true, (event) => { + const result = await api.executeUnifiedPromptStream(slug, null, null, true, true, (event) => { // Progress callback: update UI in real-time if (event.type === 'execution_started') { setProgress({ total_nodes: 0, completed_nodes: 0, current_node_label: 'Starte...' }) @@ -442,7 +443,14 @@ export default function Analysis() { } } - setNewResult({ scope: slug, content, metadata }) + setNewResult({ + scope: slug, + content, + metadata, + node_states: result.node_states, // For workflow debug panel + result_type: result.type, + aggregated_result: result.aggregated_result + }) await loadAll() setTab('run') } catch(e) { @@ -534,6 +542,9 @@ export default function Analysis() { defaultOpen={true} prompts={prompts} /> + {newResult.node_states && ( + + )} )} @@ -591,6 +602,9 @@ export default function Analysis() { defaultOpen={true} prompts={prompts} /> + {newResult.node_states && ( + + )} )} {activeCategoryKey && (() => {