Backend: - question_augmenter.py (290 Zeilen): Hybrid-Modell für Fragenergänzungen * merge_question_augmentations(): Knotengebundene Fragen überschreiben Prompt-Defaults * augment_prompt_with_questions(): Markdown-formatierte Fragenergänzung * parse_question_augmentations_from_jsonb(): JSONB → QuestionAugmentation[] - result_container_parser.py (250 Zeilen): Markdown-Sektionen-Parsing * parse_result_container(): Extrahiert Analysekern, Entscheidungsanteil, Begründungsanker * validate_decision_signal(): Normalisierung gegen answer_spectrum * Fallback-Parsing bei unstrukturierten Antworten - routers/workflow_questions.py (236 Zeilen): CRUD für workflow_question_catalog * GET /api/workflow/questions (mit active_only Filter) * POST/PUT/DELETE (Admin only, Soft Delete) - prompt_executor.py: Integration in execute_base_prompt() * Fragenergänzung vor LLM-Call (wenn node_questions oder catalog vorhanden) * Result-Container-Parsing nach LLM-Response - main.py: Router-Registrierung (workflow_questions) Tests: - test_phase1_question_augmenter.py (8 Tests): Hybrid-Modell, Formatierung, JSONB-Parsing - test_phase1_result_container_parser.py (17 Tests): Sektion-Extraktion, Decision-Parsing, Validierung Alle 25 Unit-Tests bestanden. version: 0.9j (backend) module: workflow 0.2.0 Konzept: .claude/task/Workflow_engine_prompting_engine/konzept_workflow_engine_konsolidated.md (Phase 1)
290 lines
8.2 KiB
Python
290 lines
8.2 KiB
Python
"""
|
|
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.
|
|
|
|
Format:
|
|
```
|
|
- Relevanz: [ja/nein/unklar]
|
|
- Priorität: [hoch/mittel/niedrig/unklar]
|
|
```
|
|
|
|
Args:
|
|
questions: Liste von QuestionAugmentation-Objekten
|
|
|
|
Returns:
|
|
Formatierte Markdown-Liste
|
|
"""
|
|
lines = []
|
|
for q in questions:
|
|
spectrum_str = "/".join(q.answer_spectrum)
|
|
lines.append(f"- **{q.type.capitalize()}**: [{spectrum_str}]")
|
|
|
|
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
|