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. +
+
+ {node.error}
+
+
+ {node.debug_prompt}
+
+
+ {node.debug_raw_response}
+
+
+ {node.analysis_core}
+
+ {node.error}
+ {node.debug_prompt}
+ {node.debug_raw_response}
+ {node.analysis_core}
+
- {typeof node.output === 'string'
- ? node.output
- : JSON.stringify(node.output, null, 2)}
+
+ {typeof node.output === 'string' ? node.output : JSON.stringify(node.output, null, 2)}
+
{JSON.stringify(node.metadata, null, 2)}