fix: Frontend-Backend field name mismatch for workflow questions
Root Cause:
- Frontend serialized as "questions"
- Backend expected "question_augmentations"
- Analysis Nodes WITH questions configured sent empty array to backend
- Questions were never added to LLM prompt
Frontend workflowSerializer.js:
- Serialization: questions → question_augmentations (Backend field name)
- Deserialization: question_augmentations → questions (Frontend data object)
- Backward compatible: Falls back to "questions" for old workflows
Backend workflow_executor.py:
- Removed incorrect load_prompt_questions() function (was a misunderstanding)
- Back to original logic: Only use node.question_augmentations
- Simplified normalization logging
Impact:
- Analysis Node questions are now correctly sent to backend
- Questions augment the base prompt as intended
- LLM receives structured questions
- Decision signals are generated and accessible as placeholders
Example:
- Node configures question with id="q21"
- Signal becomes accessible as {{ node_2.signal_q21 }}
- Can be used in Logic Nodes and End Node templates
Issue: Workflow questions not sent to LLM (field name mismatch)
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
b17bec3340
commit
856a82ec1d
|
|
@ -281,21 +281,13 @@ async def execute_node(
|
||||||
prompt_template = await load_prompt_template(node.prompt_slug, context)
|
prompt_template = await load_prompt_template(node.prompt_slug, context)
|
||||||
logger.debug(f"Node {node.id}: Loaded prompt '{node.prompt_slug}'")
|
logger.debug(f"Node {node.id}: Loaded prompt '{node.prompt_slug}'")
|
||||||
|
|
||||||
# 2. Parse question_augmentations (Hybrid Model)
|
# 2. Parse question_augmentations
|
||||||
questions = []
|
questions = []
|
||||||
if node.question_augmentations:
|
if node.question_augmentations:
|
||||||
# Node-specific questions (override base prompt questions)
|
# Convert list of dicts to JSONB-like format for parser
|
||||||
questions_jsonb = [q.model_dump() if hasattr(q, 'model_dump') else q for q in node.question_augmentations]
|
questions_jsonb = [q.model_dump() if hasattr(q, 'model_dump') else q for q in node.question_augmentations]
|
||||||
questions = parse_question_augmentations_from_jsonb(questions_jsonb)
|
questions = parse_question_augmentations_from_jsonb(questions_jsonb)
|
||||||
logger.debug(f"Node {node.id}: {len(questions)} node-specific questions")
|
logger.debug(f"Node {node.id}: {len(questions)} question augmentations")
|
||||||
else:
|
|
||||||
# Fallback: Load questions from base prompt (Hybrid Model)
|
|
||||||
base_questions = await load_prompt_questions(node.prompt_slug)
|
|
||||||
if base_questions:
|
|
||||||
questions = parse_question_augmentations_from_jsonb(base_questions)
|
|
||||||
logger.debug(f"Node {node.id}: {len(questions)} questions from base prompt '{node.prompt_slug}'")
|
|
||||||
else:
|
|
||||||
logger.debug(f"Node {node.id}: No questions (neither node-specific nor base prompt)")
|
|
||||||
|
|
||||||
# 3. Augment Prompt
|
# 3. Augment Prompt
|
||||||
if questions:
|
if questions:
|
||||||
|
|
@ -303,10 +295,8 @@ async def execute_node(
|
||||||
base_prompt=prompt_template,
|
base_prompt=prompt_template,
|
||||||
questions=questions
|
questions=questions
|
||||||
)
|
)
|
||||||
logger.debug(f"Node {node.id}: Augmented prompt with {len(questions)} questions")
|
|
||||||
else:
|
else:
|
||||||
augmented_prompt = prompt_template
|
augmented_prompt = prompt_template
|
||||||
logger.debug(f"Node {node.id}: No augmentation (no questions)")
|
|
||||||
|
|
||||||
# 4. LLM Call
|
# 4. LLM Call
|
||||||
logger.debug(f"Node {node.id}: Calling LLM")
|
logger.debug(f"Node {node.id}: Calling LLM")
|
||||||
|
|
@ -322,17 +312,16 @@ async def execute_node(
|
||||||
# 6. Normalize Signals
|
# 6. Normalize Signals
|
||||||
normalized_signals = []
|
normalized_signals = []
|
||||||
if parsed["decision_signals"]:
|
if parsed["decision_signals"]:
|
||||||
# Hybrid Model: Questions (node-specific or base prompt) override Catalog
|
# Hybrid Model: Node-spezifische Questions überschreiben Catalog
|
||||||
node_catalog = catalog.copy()
|
node_catalog = catalog.copy()
|
||||||
if questions:
|
if questions:
|
||||||
for q in questions:
|
for q in questions:
|
||||||
q_dict = q.model_dump() if hasattr(q, 'model_dump') else q
|
q_dict = q.model_dump() if hasattr(q, 'model_dump') else q
|
||||||
node_catalog[q_dict['type']] = {
|
node_catalog[q_dict['type']] = {
|
||||||
"answer_spectrum": q_dict['answer_spectrum'],
|
"answer_spectrum": q_dict['answer_spectrum'],
|
||||||
"normalization_rules": None # Questions haben keine Synonyme
|
"normalization_rules": None # Node-Questions haben keine Synonyme
|
||||||
}
|
}
|
||||||
source = "node-specific" if node.question_augmentations else "base prompt"
|
logger.debug(f"Node {node.id}: Override catalog for '{q_dict['type']}' with node-specific spectrum")
|
||||||
logger.debug(f"Node {node.id}: Override catalog for '{q_dict['type']}' with {source} spectrum")
|
|
||||||
|
|
||||||
normalized_signals = normalize_all_signals(
|
normalized_signals = normalize_all_signals(
|
||||||
decision_signals=parsed["decision_signals"],
|
decision_signals=parsed["decision_signals"],
|
||||||
|
|
@ -827,50 +816,6 @@ async def load_prompt_template(prompt_slug: str, context: Dict[str, Any]) -> str
|
||||||
return resolved
|
return resolved
|
||||||
|
|
||||||
|
|
||||||
async def load_prompt_questions(prompt_slug: str) -> List[Dict]:
|
|
||||||
"""
|
|
||||||
Lädt Fragen aus einem Basis-Prompt (Hybrid Model - Fallback).
|
|
||||||
|
|
||||||
Wenn ein Analysis Node KEINE node-spezifischen Fragen hat,
|
|
||||||
werden die Fragen aus dem referenzierten Basis-Prompt geladen.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
prompt_slug: Slug des Prompts (z.B. "pipeline_body")
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Liste von Question-Dicts im format:
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"id": "q1",
|
|
||||||
"type": "relevanz",
|
|
||||||
"question": "Ist eine vertiefte Analyse relevant?",
|
|
||||||
"answer_spectrum": ["ja", "nein", "unklar"]
|
|
||||||
},
|
|
||||||
...
|
|
||||||
]
|
|
||||||
|
|
||||||
Beispiel:
|
|
||||||
>>> questions = await load_prompt_questions("pipeline_body")
|
|
||||||
>>> len(questions) > 0
|
|
||||||
True
|
|
||||||
"""
|
|
||||||
with get_db() as conn:
|
|
||||||
cur = get_cursor(conn)
|
|
||||||
cur.execute(
|
|
||||||
"SELECT questions FROM ai_prompts WHERE slug = %s AND active = true",
|
|
||||||
(prompt_slug,)
|
|
||||||
)
|
|
||||||
row = cur.fetchone()
|
|
||||||
if not row or not row.get('questions'):
|
|
||||||
return []
|
|
||||||
|
|
||||||
questions = row['questions']
|
|
||||||
# PostgreSQL JSONB wird automatisch zu Python list/dict konvertiert
|
|
||||||
if isinstance(questions, list):
|
|
||||||
return questions
|
|
||||||
else:
|
|
||||||
logger.warning(f"Unexpected questions format for {prompt_slug}: {type(questions)}")
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
def aggregate_results(node_states: List[NodeExecutionState]) -> Dict[str, Any]:
|
def aggregate_results(node_states: List[NodeExecutionState]) -> Dict[str, Any]:
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ export function serializeToWorkflowGraph(nodes, edges, metadata = {}) {
|
||||||
...(node.type === 'analysis' && {
|
...(node.type === 'analysis' && {
|
||||||
prompt_slug: node.data.prompt_slug || null,
|
prompt_slug: node.data.prompt_slug || null,
|
||||||
prompt_name: node.data.prompt_name || null,
|
prompt_name: node.data.prompt_name || null,
|
||||||
questions: node.data.questions || [],
|
question_augmentations: node.data.questions || [], // Backend erwartet question_augmentations
|
||||||
fallback_strategy: node.data.fallback_strategy || 'conservative_skip'
|
fallback_strategy: node.data.fallback_strategy || 'conservative_skip'
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|
@ -85,7 +85,7 @@ export function deserializeFromWorkflowGraph(jsonbData) {
|
||||||
...(node.type === 'analysis' && {
|
...(node.type === 'analysis' && {
|
||||||
prompt_slug: node.prompt_slug || node.prompt_id || null, // Fallback für alte Workflows mit prompt_id
|
prompt_slug: node.prompt_slug || node.prompt_id || null, // Fallback für alte Workflows mit prompt_id
|
||||||
prompt_name: node.prompt_name || null, // Falls vom Backend mitgeliefert
|
prompt_name: node.prompt_name || null, // Falls vom Backend mitgeliefert
|
||||||
questions: node.questions || [],
|
questions: node.question_augmentations || node.questions || [], // Backend sendet question_augmentations
|
||||||
fallback_strategy: node.fallback_strategy || 'conservative_skip'
|
fallback_strategy: node.fallback_strategy || 'conservative_skip'
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user