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>
This commit is contained in:
parent
4c9e0e3c98
commit
ba773e677b
|
|
@ -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)"
|
new_display_name = f"{original.get('display_name') or original['name']} (Kopie)"
|
||||||
|
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""INSERT INTO ai_prompts (id, name, slug, display_name, description, template, category, active, sort_order, created, updated)
|
"""INSERT INTO ai_prompts (
|
||||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)""",
|
id, name, slug, display_name, description, template, category,
|
||||||
(new_id, new_name, new_slug, new_display_name, original['description'], original['template'],
|
type, stages, output_format, output_schema,
|
||||||
original.get('category', 'ganzheitlich'), original['active'], original['sort_order'])
|
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}
|
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'),
|
'stages': p.get('stages'),
|
||||||
'output_format': p.get('output_format', 'text'),
|
'output_format': p.get('output_format', 'text'),
|
||||||
'output_schema': p.get('output_schema'),
|
'output_schema': p.get('output_schema'),
|
||||||
|
'question_augmentations': p.get('question_augmentations'),
|
||||||
|
'graph_data': p.get('graph_data'),
|
||||||
'active': p.get('active', True),
|
'active': p.get('active', True),
|
||||||
'sort_order': p.get('sort_order', 0)
|
'sort_order': p.get('sort_order', 0)
|
||||||
}
|
}
|
||||||
|
|
@ -1689,26 +1705,39 @@ def import_prompts(
|
||||||
skipped += 1
|
skipped += 1
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Prepare stages JSON if present
|
# Prepare JSON fields if present
|
||||||
stages_json = None
|
stages_json = None
|
||||||
if p.get('stages'):
|
if p.get('stages'):
|
||||||
stages_json = json.dumps(p['stages']) if isinstance(p['stages'], list) else p['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:
|
if existing:
|
||||||
# Update existing
|
# Update existing
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
UPDATE ai_prompts SET
|
UPDATE ai_prompts SET
|
||||||
name=%s, display_name=%s, description=%s, type=%s,
|
name=%s, display_name=%s, description=%s, type=%s,
|
||||||
category=%s, template=%s, stages=%s, output_format=%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
|
updated=CURRENT_TIMESTAMP
|
||||||
WHERE slug=%s
|
WHERE slug=%s
|
||||||
""", (
|
""", (
|
||||||
p.get('name'), p.get('display_name'), p.get('description'),
|
p.get('name'), p.get('display_name'), p.get('description'),
|
||||||
p.get('type', 'pipeline'), p.get('category', 'ganzheitlich'),
|
p.get('type', 'pipeline'), p.get('category', 'ganzheitlich'),
|
||||||
p.get('template'), stages_json, p.get('output_format', 'text'),
|
p.get('template'), stages_json, p.get('output_format', 'text'),
|
||||||
p.get('output_schema'), p.get('active', True),
|
output_schema_json, question_aug_json, graph_data_json,
|
||||||
p.get('sort_order', 0), slug
|
p.get('active', True), p.get('sort_order', 0), slug
|
||||||
))
|
))
|
||||||
updated += 1
|
updated += 1
|
||||||
else:
|
else:
|
||||||
|
|
@ -1717,13 +1746,15 @@ def import_prompts(
|
||||||
INSERT INTO ai_prompts (
|
INSERT INTO ai_prompts (
|
||||||
slug, name, display_name, description, type, category,
|
slug, name, display_name, description, type, category,
|
||||||
template, stages, output_format, output_schema,
|
template, stages, output_format, output_schema,
|
||||||
|
question_augmentations, graph_data,
|
||||||
active, sort_order, created, updated
|
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'),
|
slug, p.get('name'), p.get('display_name'), p.get('description'),
|
||||||
p.get('type', 'pipeline'), p.get('category', 'ganzheitlich'),
|
p.get('type', 'pipeline'), p.get('category', 'ganzheitlich'),
|
||||||
p.get('template'), stages_json, p.get('output_format', 'text'),
|
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
|
created += 1
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -597,7 +597,14 @@ def execute_end_node(
|
||||||
combined_analysis = []
|
combined_analysis = []
|
||||||
for node_id, node_state in context.get("node_results", {}).items():
|
for node_id, node_state in context.get("node_results", {}).items():
|
||||||
if node_state.status == NodeStatus.EXECUTED and node_state.analysis_core:
|
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]"
|
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
|
final_output = state.analysis_core
|
||||||
else:
|
else:
|
||||||
# Regular node - add to combined analysis
|
# 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:
|
if state.normalized_signals:
|
||||||
all_signals.extend([s.model_dump() for s in state.normalized_signals])
|
all_signals.extend([s.model_dump() for s in state.normalized_signals])
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ export function WorkflowCanvas({
|
||||||
className="workflow-canvas"
|
className="workflow-canvas"
|
||||||
minZoom={0.2}
|
minZoom={0.2}
|
||||||
maxZoom={2}
|
maxZoom={2}
|
||||||
|
deleteKeyCode={['Backspace', 'Delete']}
|
||||||
defaultEdgeOptions={{
|
defaultEdgeOptions={{
|
||||||
animated: false,
|
animated: false,
|
||||||
style: { strokeWidth: 2 },
|
style: { strokeWidth: 2 },
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,14 @@ function validateStructure(nodes, edges, errors, warnings) {
|
||||||
message: 'Kein END-Node vorhanden',
|
message: 'Kein END-Node vorhanden',
|
||||||
severity: 'error'
|
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
|
// Zyklen-Erkennung
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user