From 856a82ec1d5ec39eedc68d53cceb9ae9a22ceab7 Mon Sep 17 00:00:00 2001 From: Lars Date: Thu, 9 Apr 2026 18:28:54 +0200 Subject: [PATCH] fix: Frontend-Backend field name mismatch for workflow questions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- backend/workflow_executor.py | 67 +++--------------------- frontend/src/utils/workflowSerializer.js | 4 +- 2 files changed, 8 insertions(+), 63 deletions(-) diff --git a/backend/workflow_executor.py b/backend/workflow_executor.py index 16a1482..e30127a 100644 --- a/backend/workflow_executor.py +++ b/backend/workflow_executor.py @@ -281,21 +281,13 @@ async def execute_node( prompt_template = await load_prompt_template(node.prompt_slug, context) logger.debug(f"Node {node.id}: Loaded prompt '{node.prompt_slug}'") - # 2. Parse question_augmentations (Hybrid Model) + # 2. Parse question_augmentations questions = [] 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 = parse_question_augmentations_from_jsonb(questions_jsonb) - logger.debug(f"Node {node.id}: {len(questions)} node-specific questions") - 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)") + logger.debug(f"Node {node.id}: {len(questions)} question augmentations") # 3. Augment Prompt if questions: @@ -303,10 +295,8 @@ async def execute_node( base_prompt=prompt_template, questions=questions ) - logger.debug(f"Node {node.id}: Augmented prompt with {len(questions)} questions") else: augmented_prompt = prompt_template - logger.debug(f"Node {node.id}: No augmentation (no questions)") # 4. LLM Call logger.debug(f"Node {node.id}: Calling LLM") @@ -322,17 +312,16 @@ async def execute_node( # 6. Normalize Signals normalized_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() if questions: for q in questions: q_dict = q.model_dump() if hasattr(q, 'model_dump') else q node_catalog[q_dict['type']] = { "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 {source} spectrum") + logger.debug(f"Node {node.id}: Override catalog for '{q_dict['type']}' with node-specific spectrum") normalized_signals = normalize_all_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 -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]: diff --git a/frontend/src/utils/workflowSerializer.js b/frontend/src/utils/workflowSerializer.js index 008f2e9..40048f6 100644 --- a/frontend/src/utils/workflowSerializer.js +++ b/frontend/src/utils/workflowSerializer.js @@ -23,7 +23,7 @@ export function serializeToWorkflowGraph(nodes, edges, metadata = {}) { ...(node.type === 'analysis' && { prompt_slug: node.data.prompt_slug || 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' }), @@ -85,7 +85,7 @@ export function deserializeFromWorkflowGraph(jsonbData) { ...(node.type === 'analysis' && { 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 - questions: node.questions || [], + questions: node.question_augmentations || node.questions || [], // Backend sendet question_augmentations fallback_strategy: node.fallback_strategy || 'conservative_skip' }),