""" Question Augmenter (Phase 1) Generiert Fragenergänzungs-Suffix für Analyseprompts. Konzept-Basis: konzept_workflow_engine_konsolidated.md (Sektion 6.4, 8.3) Anforderungsanalyse: anforderungsanalyse_umsetzungsplan.md (Sektion 4.2, 6) Hybridmodell (Sektion 6): - Primär: Knotengebundene Fragenergänzungen (am Workflow-Knoten definiert) - Sekundär: Prompt-gebundene Standardfragen (optional in ai_prompts.question_augmentations) - Vorrangregel: Knotenspezifische überschreiben Prompt-Defaults Output-Format (User-Entscheidung 2): - Markdown-Sektionen mit klaren Delimitern (statt verpflichtendem JSON-Mode) """ from typing import List, Dict, Optional, Any from workflow_models import QuestionAugmentation from db import get_db, get_cursor, r2d def generate_question_suffix(questions: List[QuestionAugmentation]) -> str: """ Generiert Fragenergänzungs-Suffix für einen Analyseprompt. Format (Markdown-Sektionen): ``` ## Analyse [Hauptinhalt deines Prompts hier] ## Entscheidungsfragen Beantworte folgende Fragen präzise: - Relevanz: [ja/nein/unklar] - Priorität: [hoch/mittel/niedrig/unklar] ## Begründung [Optional: Kurze Plausibilisierung deiner Antworten (1-2 Sätze)] ``` Args: questions: Liste von QuestionAugmentation-Objekten Returns: Fragenergänzungs-Suffix als Markdown-String Raises: ValueError: Bei leerer Fragenliste """ if not questions: raise ValueError("Fragenliste darf nicht leer sein") # Build question list question_lines = [] for q in questions: # Format: "- Fragentyp: [erlaubte Werte]" spectrum_str = "/".join(q.answer_spectrum) question_lines.append(f"- {q.type.capitalize()}: [{spectrum_str}]") question_block = "\n".join(question_lines) suffix = f""" --- **WICHTIG: Strukturiere deine Antwort wie folgt:** ## Analyse [Deine Hauptanalyse hier - beantworte die ursprüngliche Frage ausführlich] ## Entscheidungsfragen Beantworte folgende Fragen **präzise** mit den vorgegebenen Werten: {question_block} ## Begründung [Optional: Kurze Plausibilisierung deiner Entscheidungsfragen-Antworten (1-2 Sätze)] **Hinweise:** - Antworte bei Entscheidungsfragen NUR mit den vorgegebenen Werten - Bei Unsicherheit wähle "unklar" - Die Begründung ist optional, aber hilfreich für die Nachvollziehbarkeit """ return suffix def load_question_augmentations_from_db( question_types: Optional[List[str]] = None ) -> List[QuestionAugmentation]: """ Lädt Fragenergänzungen aus der Datenbank (workflow_question_catalog). Args: question_types: Optionale Liste von Fragetypen zum Filtern (z.B. ["relevanz", "prioritaet"]) Wenn None: Alle aktiven Fragen werden geladen Returns: Liste von QuestionAugmentation-Objekten Raises: HTTPException: Bei Datenbankfehlern """ with get_db() as conn: cur = get_cursor(conn) if question_types: placeholders = ", ".join(["%s"] * len(question_types)) cur.execute( f"""SELECT id, question_type, label, question_template, answer_spectrum FROM workflow_question_catalog WHERE active = true AND question_type IN ({placeholders}) ORDER BY question_type""", tuple(question_types) ) else: cur.execute( """SELECT id, question_type, label, question_template, answer_spectrum FROM workflow_question_catalog WHERE active = true ORDER BY question_type""" ) rows = cur.fetchall() questions = [] for row in rows: db_row = r2d(row) questions.append( QuestionAugmentation( id=db_row['question_type'], # Verwende Typ als ID (z.B. "relevanz") type=db_row['question_type'], question=db_row['question_template'], answer_spectrum=db_row['answer_spectrum'] ) ) return questions def merge_question_augmentations( node_questions: Optional[List[QuestionAugmentation]], prompt_default_questions: Optional[List[QuestionAugmentation]] ) -> List[QuestionAugmentation]: """ Merged Fragenergänzungen nach Hybridmodell-Vorrangregel. Vorrangregel (Sektion 6.2 der Anforderungsanalyse): 1. Knotenspezifische Fragenergänzungen überschreiben Prompt-Defaults 2. Wenn Knoten keine Fragen definiert: Prompt-Defaults werden verwendet (falls vorhanden) 3. Wenn weder Knoten noch Prompt Fragen definieren: Leere Liste Args: node_questions: Fragenergänzungen am Workflow-Knoten (primär) prompt_default_questions: Fragenergänzungen am Prompt (sekundär) Returns: Gemergete Liste von QuestionAugmentation-Objekten """ # Vorrangregel 1: Knotenspezifische Fragen haben absolute Priorität if node_questions: return node_questions # Vorrangregel 2: Wenn Knoten keine Fragen hat, verwende Prompt-Defaults if prompt_default_questions: return prompt_default_questions # Vorrangregel 3: Wenn weder Knoten noch Prompt Fragen haben: Leere Liste return [] def parse_question_augmentations_from_jsonb( jsonb_data: Optional[Dict[str, Any]] ) -> List[QuestionAugmentation]: """ Parsed QuestionAugmentation-Objekte aus JSONB-Daten (z.B. ai_prompts.question_augmentations). Format: [ { "id": "q1", "type": "relevanz", "question": "Ist eine vertiefte Analyse relevant?", "answer_spectrum": ["ja", "nein", "unklar"] }, ... ] Args: jsonb_data: JSONB-Array als Python-Dict/List Returns: Liste von QuestionAugmentation-Objekten Raises: ValueError: Bei ungültigem Format """ if not jsonb_data: return [] if not isinstance(jsonb_data, list): raise ValueError("question_augmentations muss ein Array sein") questions = [] for item in jsonb_data: try: questions.append(QuestionAugmentation(**item)) except Exception as e: raise ValueError(f"Ungültiges QuestionAugmentation-Format: {e}") return questions def get_question_augmentation_instruction() -> str: """ Gibt generische Instruktion für strukturierte Antwort zurück. Diese Instruktion wird IMMER zum Prompt hinzugefügt, wenn Fragenergänzungen aktiv sind. Returns: Instruktions-String als Markdown """ return """ --- **WICHTIG: Strukturiere deine Antwort in folgenden Markdown-Sektionen:** 1. **## Analyse** - Deine Hauptanalyse (beantworte die ursprüngliche Frage) 2. **## Entscheidungsfragen** - Beantworte die unten stehenden Fragen PRÄZISE mit den vorgegebenen Werten 3. **## Begründung** - Optional: Kurze Plausibilisierung deiner Entscheidungsfragen (1-2 Sätze) """ def format_question_list(questions: List[QuestionAugmentation]) -> str: """ Formatiert Fragenliste als Markdown-Liste. Verwendet question.id als Schlüssel (nicht type), damit mehrere Fragen des gleichen Typs möglich sind. Format: ``` - q21: [ja/nein/unklar] # Ist Protein unsicher? - q22: [ja/nein/unklar] # Ist Energie unsicher? ``` Args: questions: Liste von QuestionAugmentation-Objekten Returns: Formatierte Markdown-Liste """ lines = [] for q in questions: spectrum_str = "/".join(q.answer_spectrum) # Use ID as key (unique), show question text as comment for context question_text = q.question[:50] if q.question else q.type lines.append(f"- **{q.id}**: [{spectrum_str}] # {question_text}") return "\n".join(lines) def augment_prompt_with_questions( base_prompt: str, questions: List[QuestionAugmentation] ) -> str: """ Fügt Fragenergänzungen zu einem Basis-Prompt hinzu. Args: base_prompt: Original-Prompt-Text questions: Liste von Fragenergänzungen Returns: Erweiterter Prompt mit Fragenergänzungen Raises: ValueError: Bei leerer Fragenliste """ if not questions: raise ValueError("Keine Fragenergänzungen vorhanden") instruction = get_question_augmentation_instruction() question_list = format_question_list(questions) augmented_prompt = f"""{base_prompt} {instruction} **Entscheidungsfragen:** {question_list} """ return augmented_prompt