ROOT ARCHITECTURAL CHANGE:
Multiple questions with same type are now supported!
Problem:
- question_augmenter used q.type as LLM key
- If two questions had type="unsicherheit":
- LLM saw duplicate keys: "- unsicherheit: [ja/nein]"
- Could only answer one
- Signals were ambiguous
Solution:
- Use question.id as LLM key (unique by design)
- Keep type for normalization logic
- Map id → type internally
Backend question_augmenter.py:
- format_question_list() now uses q.id as key
- Format: "- **q21**: [ja/nein] # Question text"
- Question text as comment for LLM context
Backend workflow_executor.py:
- Removed type→id mapping (no longer needed)
- decision_signals now keyed by id (from LLM)
- Build id→type catalog for normalization
- NormalizedSignal.question_type stores id (not type!)
- End Node template: signal_{id} directly available
Flow:
1. Questions sent to LLM: "- q21: [ja/nein] # Ist Protein unsicher?"
2. LLM answers: "- q21: nein"
3. Normalization: id→type lookup for spectrum/rules
4. Template: {{ node_4.signal_q21 }} = "nein"
Example (TWO unsicherheit questions):
Questions:
- q21: type=unsicherheit, question="Ist Protein unsicher?"
- q22: type=unsicherheit, question="Ist Energie unsicher?"
LLM Prompt:
```
## Entscheidungsfragen
- **q21**: [ja/nein] # Ist Protein unsicher?
- **q22**: [ja/nein] # Ist Energie unsicher?
```
LLM Response:
```
- q21: nein
- q22: ja
```
Template:
```
{{ node_4.signal_q21 }} → "nein"
{{ node_4.signal_q22 }} → "ja"
```
BREAKING CHANGE:
- Old workflows with decision_signals keyed by type will break
- Need to re-execute workflows after update
Issue: Cannot have multiple questions with same type
Version: 0.9p (workflow module)
Part 3: End Node Template Engine - ARCHITECTURAL FIX
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
295 lines
8.4 KiB
Python
295 lines
8.4 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.
|
|
|
|
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
|