From ba773e677b34b08737722bc6f3d9c12d0b790044 Mon Sep 17 00:00:00 2001 From: Lars Date: Sat, 11 Apr 2026 14:15:57 +0200 Subject: [PATCH] fix(workflow): Test-Suite Fixes - Issues #5, #8, #9, #11, #12 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- backend/routers/prompts.py | 51 +++++++++++++++---- backend/workflow_executor.py | 17 ++++++- .../components/workflow/WorkflowCanvas.jsx | 1 + frontend/src/utils/workflowValidation.js | 8 +++ 4 files changed, 65 insertions(+), 12 deletions(-) diff --git a/backend/routers/prompts.py b/backend/routers/prompts.py index 5e7d76f..2303de2 100644 --- a/backend/routers/prompts.py +++ b/backend/routers/prompts.py @@ -215,10 +215,24 @@ def duplicate_prompt(prompt_id: str, session: dict=Depends(require_admin)): new_display_name = f"{original.get('display_name') or original['name']} (Kopie)" cur.execute( - """INSERT INTO ai_prompts (id, name, slug, display_name, description, template, category, active, sort_order, created, updated) - VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)""", - (new_id, new_name, new_slug, new_display_name, original['description'], original['template'], - original.get('category', 'ganzheitlich'), original['active'], original['sort_order']) + """INSERT INTO ai_prompts ( + id, name, slug, display_name, description, template, category, + type, stages, output_format, output_schema, + question_augmentations, graph_data, + active, sort_order, created, updated + ) VALUES ( + %s, %s, %s, %s, %s, %s, %s, + %s, %s, %s, %s, + %s, %s, + %s, %s, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP + )""", + (new_id, new_name, new_slug, new_display_name, + original.get('description'), original.get('template'), + original.get('category', 'ganzheitlich'), + original.get('type', 'pipeline'), original.get('stages'), + original.get('output_format', 'text'), original.get('output_schema'), + original.get('question_augmentations'), original.get('graph_data'), + original.get('active', True), original.get('sort_order', 0)) ) return {"id": new_id, "slug": new_slug, "name": new_name} @@ -1635,6 +1649,8 @@ def export_all_prompts(session: dict = Depends(require_admin)): 'stages': p.get('stages'), 'output_format': p.get('output_format', 'text'), 'output_schema': p.get('output_schema'), + 'question_augmentations': p.get('question_augmentations'), + 'graph_data': p.get('graph_data'), 'active': p.get('active', True), 'sort_order': p.get('sort_order', 0) } @@ -1689,26 +1705,39 @@ def import_prompts( skipped += 1 continue - # Prepare stages JSON if present + # Prepare JSON fields if present stages_json = None if p.get('stages'): stages_json = json.dumps(p['stages']) if isinstance(p['stages'], list) else p['stages'] + output_schema_json = None + if p.get('output_schema'): + output_schema_json = json.dumps(p['output_schema']) if isinstance(p['output_schema'], dict) else p['output_schema'] + + question_aug_json = None + if p.get('question_augmentations'): + question_aug_json = json.dumps(p['question_augmentations']) if isinstance(p['question_augmentations'], (dict, list)) else p['question_augmentations'] + + graph_data_json = None + if p.get('graph_data'): + graph_data_json = json.dumps(p['graph_data']) if isinstance(p['graph_data'], dict) else p['graph_data'] + if existing: # Update existing cur.execute(""" UPDATE ai_prompts SET name=%s, display_name=%s, description=%s, type=%s, category=%s, template=%s, stages=%s, output_format=%s, - output_schema=%s, active=%s, sort_order=%s, + output_schema=%s, question_augmentations=%s, graph_data=%s, + active=%s, sort_order=%s, updated=CURRENT_TIMESTAMP WHERE slug=%s """, ( p.get('name'), p.get('display_name'), p.get('description'), p.get('type', 'pipeline'), p.get('category', 'ganzheitlich'), p.get('template'), stages_json, p.get('output_format', 'text'), - p.get('output_schema'), p.get('active', True), - p.get('sort_order', 0), slug + output_schema_json, question_aug_json, graph_data_json, + p.get('active', True), p.get('sort_order', 0), slug )) updated += 1 else: @@ -1717,13 +1746,15 @@ def import_prompts( INSERT INTO ai_prompts ( slug, name, display_name, description, type, category, template, stages, output_format, output_schema, + question_augmentations, graph_data, active, sort_order, created, updated - ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP) + ) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP) """, ( slug, p.get('name'), p.get('display_name'), p.get('description'), p.get('type', 'pipeline'), p.get('category', 'ganzheitlich'), p.get('template'), stages_json, p.get('output_format', 'text'), - p.get('output_schema'), p.get('active', True), p.get('sort_order', 0) + output_schema_json, question_aug_json, graph_data_json, + p.get('active', True), p.get('sort_order', 0) )) created += 1 diff --git a/backend/workflow_executor.py b/backend/workflow_executor.py index a516727..8809032 100644 --- a/backend/workflow_executor.py +++ b/backend/workflow_executor.py @@ -597,7 +597,14 @@ def execute_end_node( combined_analysis = [] for node_id, node_state in context.get("node_results", {}).items(): if node_state.status == NodeStatus.EXECUTED and node_state.analysis_core: - combined_analysis.append(f"## {node_id}\n{node_state.analysis_core}") + # Get node label from graph (fallback to node_id if not found) + node_label = node_id # default + if graph: + node_obj = next((n for n in graph.nodes if n.id == node_id), None) + if node_obj and hasattr(node_obj, 'data') and node_obj.data: + node_label = node_obj.data.get('label', node_id) + + combined_analysis.append(f"## {node_label}\n{node_state.analysis_core}") final_output = "\n\n".join(combined_analysis) if combined_analysis else "[No analysis generated]" @@ -999,7 +1006,13 @@ def aggregate_results(node_states: List[NodeExecutionState], graph) -> Dict[str, final_output = state.analysis_core else: # Regular node - add to combined analysis - combined_analysis.append(f"## {state.node_id}\n{state.analysis_core}") + # Get node label from graph (fallback to node_id) + node_label = state.node_id # default + node_obj = next((n for n in graph.nodes if n.id == state.node_id), None) + if node_obj and hasattr(node_obj, 'data') and node_obj.data: + node_label = node_obj.data.get('label', state.node_id) + + combined_analysis.append(f"## {node_label}\n{state.analysis_core}") if state.normalized_signals: all_signals.extend([s.model_dump() for s in state.normalized_signals]) diff --git a/frontend/src/components/workflow/WorkflowCanvas.jsx b/frontend/src/components/workflow/WorkflowCanvas.jsx index 447d68c..930f8c4 100644 --- a/frontend/src/components/workflow/WorkflowCanvas.jsx +++ b/frontend/src/components/workflow/WorkflowCanvas.jsx @@ -39,6 +39,7 @@ export function WorkflowCanvas({ className="workflow-canvas" minZoom={0.2} maxZoom={2} + deleteKeyCode={['Backspace', 'Delete']} defaultEdgeOptions={{ animated: false, style: { strokeWidth: 2 }, diff --git a/frontend/src/utils/workflowValidation.js b/frontend/src/utils/workflowValidation.js index 67011ad..13aa1fc 100644 --- a/frontend/src/utils/workflowValidation.js +++ b/frontend/src/utils/workflowValidation.js @@ -49,6 +49,14 @@ function validateStructure(nodes, edges, errors, warnings) { 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