Universal CSV Importer #70

Merged
Lars merged 54 commits from develop into main 2026-04-11 07:06:47 +02:00
2 changed files with 45 additions and 41 deletions
Showing only changes of commit de5b8cbf15 - Show all commits

View File

@ -235,10 +235,13 @@ def format_question_list(questions: List[QuestionAugmentation]) -> str:
""" """
Formatiert Fragenliste als Markdown-Liste. Formatiert Fragenliste als Markdown-Liste.
Verwendet question.id als Schlüssel (nicht type), damit mehrere Fragen
des gleichen Typs möglich sind.
Format: Format:
``` ```
- Relevanz: [ja/nein/unklar] - q21: [ja/nein/unklar] # Ist Protein unsicher?
- Priorität: [hoch/mittel/niedrig/unklar] - q22: [ja/nein/unklar] # Ist Energie unsicher?
``` ```
Args: Args:
@ -250,7 +253,9 @@ def format_question_list(questions: List[QuestionAugmentation]) -> str:
lines = [] lines = []
for q in questions: for q in questions:
spectrum_str = "/".join(q.answer_spectrum) spectrum_str = "/".join(q.answer_spectrum)
lines.append(f"- **{q.type.capitalize()}**: [{spectrum_str}]") # 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) return "\n".join(lines)

View File

@ -28,7 +28,7 @@ from question_augmenter import (
parse_question_augmentations_from_jsonb parse_question_augmentations_from_jsonb
) )
from result_container_parser import parse_result_container from result_container_parser import parse_result_container
from normalization_engine import normalize_all_signals, load_question_catalog from normalization_engine import normalize_all_signals, normalize_signal_value, load_question_catalog
from logic_evaluator import evaluate_logic_expression, resolve_signal_reference from logic_evaluator import evaluate_logic_expression, resolve_signal_reference
from join_evaluator import evaluate_join_node as evaluate_join_node_core from join_evaluator import evaluate_join_node as evaluate_join_node_core
from db import get_db, get_cursor from db import get_db, get_cursor
@ -311,23 +311,45 @@ async def execute_node(
logger.debug(f"Node {node.id}: Parsed response (status: {parsed['parsing_status']})") logger.debug(f"Node {node.id}: Parsed response (status: {parsed['parsing_status']})")
# 6. Normalize Signals # 6. Normalize Signals
# NOTE: decision_signals now use question.id as key (not type)
# We need to build a catalog: id → {type, spectrum} for normalization
normalized_signals = [] normalized_signals = []
if parsed["decision_signals"]: if parsed["decision_signals"]:
# Hybrid Model: Node-spezifische Questions überschreiben Catalog # Build catalog: id → answer_spectrum (for normalization)
node_catalog = catalog.copy() id_catalog = {}
if questions: if questions:
for q in questions: for q in questions:
q_dict = q.model_dump() if hasattr(q, 'model_dump') else q q_dict = q.model_dump() if hasattr(q, 'model_dump') else q
node_catalog[q_dict['type']] = { id_catalog[q_dict['id']] = {
"type": q_dict['type'], # Keep type for normalization
"answer_spectrum": q_dict['answer_spectrum'], "answer_spectrum": q_dict['answer_spectrum'],
"normalization_rules": None # Node-Questions haben keine Synonyme "normalization_rules": None # Node-Questions haben keine Synonyme
} }
logger.debug(f"Node {node.id}: Override catalog for '{q_dict['type']}' with node-specific spectrum")
normalized_signals = normalize_all_signals( # Normalize each signal (signals keyed by ID now)
decision_signals=parsed["decision_signals"], for signal_id, signal_value in parsed["decision_signals"].items():
catalog_dict=node_catalog if signal_id in id_catalog:
) q_config = id_catalog[signal_id]
# Use the type-based catalog for normalization rules (if any)
type_catalog_entry = catalog.get(q_config['type'], {})
# Normalize with question-specific spectrum
normalized = normalize_signal_value(
raw_value=signal_value,
answer_spectrum=q_config['answer_spectrum'],
normalization_rules=type_catalog_entry.get('normalization_rules')
)
normalized_signals.append(NormalizedSignal(
question_type=signal_id, # Store ID as question_type (for template access)
raw_value=signal_value,
normalized_value=normalized.get('normalized_value'),
status=normalized.get('status'),
confidence=normalized.get('confidence'),
metadata=normalized.get('metadata')
))
logger.debug(f"Node {node.id}: Normalized signal '{signal_id}' = '{signal_value}''{normalized.get('normalized_value')}'")
logger.info(f"Node {node.id}: Normalized {len(normalized_signals)} signals") logger.info(f"Node {node.id}: Normalized {len(normalized_signals)} signals")
return NodeExecutionState( return NodeExecutionState(
@ -603,41 +625,18 @@ def execute_end_node(
"status": node_state.status.value if node_state.status else "unknown", "status": node_state.status.value if node_state.status else "unknown",
} }
# Build direct question_type → question_id mapping
question_type_to_id = {}
if graph:
workflow_node = next((n for n in graph.nodes if n.id == node_id), None)
if workflow_node and workflow_node.question_augmentations:
for q in workflow_node.question_augmentations:
q_dict = q.model_dump() if hasattr(q, 'model_dump') else q
q_type = q_dict.get('type')
q_id = q_dict.get('id')
if q_type and q_id:
# WICHTIG: Wenn mehrere Fragen den gleichen type haben, ist das ein Fehler!
if q_type in question_type_to_id:
logger.error(
f"DUPLICATE question type '{q_type}'! "
f"First ID: {question_type_to_id[q_type]}, Second ID: {q_id}. "
f"Each question MUST have a UNIQUE type!"
)
question_type_to_id[q_type] = q_id
# Add normalized signals as {{node_id.signal_ID}} # Add normalized signals as {{node_id.signal_ID}}
# NOTE: question_type now IS the ID (not the type!)
if node_state.normalized_signals: if node_state.normalized_signals:
for signal in node_state.normalized_signals: for signal in node_state.normalized_signals:
# Convert NormalizedSignal object to dict if needed # Convert NormalizedSignal object to dict if needed
signal_dict = signal.model_dump() if hasattr(signal, 'model_dump') else signal signal_dict = signal.model_dump() if hasattr(signal, 'model_dump') else signal
q_type = signal_dict['question_type'] q_id = signal_dict['question_type'] # This is actually the ID now!
# Direct lookup: question_type → question_id signal_key = f"signal_{q_id}"
if q_type in question_type_to_id: signal_value = signal_dict['normalized_value'] or signal_dict['raw_value']
q_id = question_type_to_id[q_type] node_context[signal_key] = signal_value
signal_key = f"signal_{q_id}" logger.info(f"Mapped signal: {q_id}{signal_key} = '{signal_value}'")
signal_value = signal_dict['normalized_value'] or signal_dict['raw_value']
node_context[signal_key] = signal_value
logger.info(f"Mapped signal: {q_type}{signal_key} = '{signal_value}'")
else:
logger.warning(f"No question_id found for signal type='{q_type}' (available types: {list(question_type_to_id.keys())})")
# Add question texts as {{node_id.question_ID}} # Add question texts as {{node_id.question_ID}}
if graph: if graph: