fix: CRITICAL - Use question ID in placeholders, not type
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 <noreply@anthropic.com>
This commit is contained in:
parent
f9c244bf48
commit
3b4902dc11
|
|
@ -603,22 +603,56 @@ def execute_end_node(
|
||||||
"status": node_state.status.value if node_state.status else "unknown",
|
"status": node_state.status.value if node_state.status else "unknown",
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add normalized signals as {{node_id.signal_X}}
|
# Build question_type → question_id mapping from workflow graph
|
||||||
if node_state.normalized_signals:
|
question_id_map = {}
|
||||||
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}}
|
|
||||||
if graph:
|
if graph:
|
||||||
workflow_node = next((n for n in graph.nodes if n.id == node_id), None)
|
workflow_node = next((n for n in graph.nodes if n.id == node_id), None)
|
||||||
if workflow_node and workflow_node.question_augmentations:
|
if workflow_node and workflow_node.question_augmentations:
|
||||||
for q in workflow_node.question_augmentations:
|
for q in workflow_node.question_augmentations:
|
||||||
q_dict = q.model_dump() if hasattr(q, 'model_dump') else q
|
q_dict = q.model_dump() if hasattr(q, 'model_dump') else q
|
||||||
question_key = f"question_{q_dict['type']}"
|
q_type = q_dict.get('type')
|
||||||
node_context[question_key] = q_dict.get('question', '')
|
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
|
template_context[node_id] = node_context
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -376,21 +376,21 @@ function extractWorkflowPlaceholders(nodes) {
|
||||||
if (node.type === 'analysis' && node.data.questions && node.data.questions.length > 0) {
|
if (node.type === 'analysis' && node.data.questions && node.data.questions.length > 0) {
|
||||||
node.data.questions.forEach((q, qIdx) => {
|
node.data.questions.forEach((q, qIdx) => {
|
||||||
const questionId = q.id || `q${qIdx + 1}`
|
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}`
|
const questionText = q.question || `Frage ${qIdx + 1}`
|
||||||
|
|
||||||
// Signal-Platzhalter (Antwort)
|
// Signal-Platzhalter (Antwort) - VERWENDET ID für Eindeutigkeit!
|
||||||
placeholders.push({
|
placeholders.push({
|
||||||
placeholder: `{{ ${nodeId}.signal_${questionType} }}`,
|
placeholder: `{{ ${nodeId}.signal_${questionId} }}`,
|
||||||
description: `${nodeLabel} (${questionId}) - Signal ${questionType}: ${questionText.substring(0, 40)}${questionText.length > 40 ? '...' : ''}`,
|
description: `${nodeLabel} - ${questionId} (${questionType}): ${questionText.substring(0, 45)}${questionText.length > 45 ? '...' : ''}`,
|
||||||
icon: '📊',
|
icon: '📊',
|
||||||
category: 'Workflow - Signals'
|
category: 'Workflow - Signals'
|
||||||
})
|
})
|
||||||
|
|
||||||
// Frage-Text-Platzhalter
|
// Frage-Text-Platzhalter
|
||||||
placeholders.push({
|
placeholders.push({
|
||||||
placeholder: `{{ ${nodeId}.question_${questionType} }}`,
|
placeholder: `{{ ${nodeId}.question_${questionId} }}`,
|
||||||
description: `${nodeLabel} (${questionId}) - Frage ${questionType}: ${questionText.substring(0, 40)}${questionText.length > 40 ? '...' : ''}`,
|
description: `${nodeLabel} - ${questionId} (${questionType}): ${questionText.substring(0, 45)}${questionText.length > 45 ? '...' : ''}`,
|
||||||
icon: '❓',
|
icon: '❓',
|
||||||
category: 'Workflow - Questions'
|
category: 'Workflow - Questions'
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user