From 3b4902dc112f015eb3e08e99d93447e3638188fe Mon Sep 17 00:00:00 2001 From: Lars Date: Thu, 9 Apr 2026 21:01:24 +0200 Subject: [PATCH] fix: CRITICAL - Use question ID in placeholders, not type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root Cause: - Multiple questions with same type (e.g. "unsicherheit") created duplicate placeholders - {{ node_4.signal_unsicherheit }} could refer to q21 OR q22 - Later signal overwrote earlier one in template context Solution: - Placeholders now use question ID: {{ node_4.signal_q21 }} - Unique even with multiple questions of same type Frontend PlaceholderPicker.jsx: - Changed placeholder: signal_${questionType} → signal_${questionId} - Changed placeholder: question_${questionType} → question_${questionId} - Description shows both: "q21 (unsicherheit): Question text" Backend workflow_executor.py: - Build question_type → question_id mapping from graph - Map normalized_signals (by type) to question IDs - Handles duplicate types with index tracking - Creates signal_${id} and question_${id} in template context Example: Questions configured: - q21: type="unsicherheit", question="Ist Protein unsicher?" - q22: type="unsicherheit", question="Ist Energie unsicher?" Placeholders generated: - {{ node_4.signal_q21 }} → "nein" - {{ node_4.signal_q22 }} → "ja" - {{ node_4.question_q21 }} → "Ist Protein unsicher?" - {{ node_4.question_q22 }} → "Ist Energie unsicher?" Issue: Duplicate question types cause placeholder conflicts Version: 0.9p (workflow module) Part 3: End Node Template Engine - CRITICAL FIX Co-Authored-By: Claude Opus 4.6 --- backend/workflow_executor.py | 56 +++++++++++++++---- .../workflow/panels/PlaceholderPicker.jsx | 12 ++-- 2 files changed, 51 insertions(+), 17 deletions(-) diff --git a/backend/workflow_executor.py b/backend/workflow_executor.py index cdadb4a..eee6080 100644 --- a/backend/workflow_executor.py +++ b/backend/workflow_executor.py @@ -603,22 +603,56 @@ def execute_end_node( "status": node_state.status.value if node_state.status else "unknown", } - # Add normalized signals as {{node_id.signal_X}} - if node_state.normalized_signals: - for signal in node_state.normalized_signals: - # Convert NormalizedSignal object to dict if needed - signal_dict = signal.model_dump() if hasattr(signal, 'model_dump') else signal - signal_key = f"signal_{signal_dict['question_type']}" - node_context[signal_key] = signal_dict['normalized_value'] or signal_dict['raw_value'] - - # Add question texts as {{node_id.question_X}} + # Build question_type → question_id mapping from workflow graph + question_id_map = {} if graph: workflow_node = next((n for n in graph.nodes if n.id == node_id), None) if workflow_node and workflow_node.question_augmentations: for q in workflow_node.question_augmentations: q_dict = q.model_dump() if hasattr(q, 'model_dump') else q - question_key = f"question_{q_dict['type']}" - node_context[question_key] = q_dict.get('question', '') + q_type = q_dict.get('type') + q_id = q_dict.get('id') + if q_type and q_id: + # Multiple questions with same type → use ID + if q_type not in question_id_map: + question_id_map[q_type] = [] + question_id_map[q_type].append(q_id) + + # Add normalized signals as {{node_id.signal_ID}} (not type!) + if node_state.normalized_signals: + # Track which types we've seen (for duplicate detection) + type_counts = {} + + for signal in node_state.normalized_signals: + # Convert NormalizedSignal object to dict if needed + signal_dict = signal.model_dump() if hasattr(signal, 'model_dump') else signal + q_type = signal_dict['question_type'] + + # Determine which ID to use for this signal + if q_type in question_id_map: + # Get the Nth ID for this type (handles duplicates) + type_idx = type_counts.get(q_type, 0) + type_counts[q_type] = type_idx + 1 + + if type_idx < len(question_id_map[q_type]): + q_id = question_id_map[q_type][type_idx] + signal_key = f"signal_{q_id}" + node_context[signal_key] = signal_dict['normalized_value'] or signal_dict['raw_value'] + else: + logger.warning(f"No question_id found for signal type={q_type} index={type_idx}") + else: + logger.warning(f"No question_id mapping for type={q_type}") + + # Add question texts as {{node_id.question_ID}} + if graph: + workflow_node = next((n for n in graph.nodes if n.id == node_id), None) + if workflow_node and workflow_node.question_augmentations: + for q in workflow_node.question_augmentations: + q_dict = q.model_dump() if hasattr(q, 'model_dump') else q + q_id = q_dict.get('id') + if q_id: + question_key = f"question_{q_id}" + node_context[question_key] = q_dict.get('question', '') template_context[node_id] = node_context diff --git a/frontend/src/components/workflow/panels/PlaceholderPicker.jsx b/frontend/src/components/workflow/panels/PlaceholderPicker.jsx index d40e983..6d2381f 100644 --- a/frontend/src/components/workflow/panels/PlaceholderPicker.jsx +++ b/frontend/src/components/workflow/panels/PlaceholderPicker.jsx @@ -376,21 +376,21 @@ function extractWorkflowPlaceholders(nodes) { if (node.type === 'analysis' && node.data.questions && node.data.questions.length > 0) { node.data.questions.forEach((q, qIdx) => { const questionId = q.id || `q${qIdx + 1}` - const questionType = q.type || `q${qIdx + 1}` + const questionType = q.type || 'unknown' const questionText = q.question || `Frage ${qIdx + 1}` - // Signal-Platzhalter (Antwort) + // Signal-Platzhalter (Antwort) - VERWENDET ID für Eindeutigkeit! placeholders.push({ - placeholder: `{{ ${nodeId}.signal_${questionType} }}`, - description: `${nodeLabel} (${questionId}) - Signal ${questionType}: ${questionText.substring(0, 40)}${questionText.length > 40 ? '...' : ''}`, + placeholder: `{{ ${nodeId}.signal_${questionId} }}`, + description: `${nodeLabel} - ${questionId} (${questionType}): ${questionText.substring(0, 45)}${questionText.length > 45 ? '...' : ''}`, icon: '📊', category: 'Workflow - Signals' }) // Frage-Text-Platzhalter placeholders.push({ - placeholder: `{{ ${nodeId}.question_${questionType} }}`, - description: `${nodeLabel} (${questionId}) - Frage ${questionType}: ${questionText.substring(0, 40)}${questionText.length > 40 ? '...' : ''}`, + placeholder: `{{ ${nodeId}.question_${questionId} }}`, + description: `${nodeLabel} - ${questionId} (${questionType}): ${questionText.substring(0, 45)}${questionText.length > 45 ? '...' : ''}`, icon: '❓', category: 'Workflow - Questions' })