From b17bec3340cae0383c5e86ae8d02aa8e373b2df7 Mon Sep 17 00:00:00 2001 From: Lars Date: Thu, 9 Apr 2026 18:18:08 +0200 Subject: [PATCH] fix: Load base prompt questions in workflow (Hybrid Model) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend workflow_executor.py: - New function: load_prompt_questions() loads questions from base prompt - execute_node() now implements Hybrid Model correctly: * IF node has question_augmentations → use those (override) * ELSE load questions from referenced base prompt (fallback) - Normalization now uses `questions` variable (not node.question_augmentations) - This fixes base prompts having questions that were ignored in workflows Root Cause: - Phase 1 Hybrid Model was incomplete - Node-specific questions worked, but base prompt questions were ignored - augment_prompt_with_questions() was only called when node.question_augmentations existed Impact: - Analysis Nodes WITHOUT custom questions now use base prompt questions - LLM receives proper question augmentation - Decision signals are generated and normalized correctly Issue: Workflow questions not sent to LLM Version: 0.9p (workflow module) Part 3: End Node Template Engine - Critical Fix Co-Authored-By: Claude Opus 4.6 --- backend/workflow_executor.py | 73 ++++++++++++++++++++++++++++++++---- 1 file changed, 65 insertions(+), 8 deletions(-) diff --git a/backend/workflow_executor.py b/backend/workflow_executor.py index 2239804..16a1482 100644 --- a/backend/workflow_executor.py +++ b/backend/workflow_executor.py @@ -281,13 +281,21 @@ 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 + # 2. Parse question_augmentations (Hybrid Model) questions = [] if node.question_augmentations: - # Convert list of dicts to JSONB-like format for parser + # Node-specific questions (override base prompt questions) 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)} question augmentations") + 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)") # 3. Augment Prompt if questions: @@ -295,8 +303,10 @@ 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") @@ -312,16 +322,17 @@ async def execute_node( # 6. Normalize Signals normalized_signals = [] if parsed["decision_signals"]: - # Hybrid Model: Node-spezifische Questions überschreiben Catalog + # Hybrid Model: Questions (node-specific or base prompt) override Catalog node_catalog = catalog.copy() - if node.question_augmentations: - for q in node.question_augmentations: + 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 # Node-Questions haben keine Synonyme + "normalization_rules": None # Questions haben keine Synonyme } - logger.debug(f"Node {node.id}: Override catalog for '{q_dict['type']}' with node-specific spectrum") + 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") normalized_signals = normalize_all_signals( decision_signals=parsed["decision_signals"], @@ -816,6 +827,52 @@ 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]: """ Aggregiert Ergebnisse aller Knoten.