fix: Support UI-format LogicExpression in Logic-Node condition field
All checks were successful
Deploy Development / deploy (push) Successful in 48s
Build Test / pytest-backend (push) Successful in 13s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 15s

Root cause: UI saves LogicExpression directly as condition:
  {operands: [...], operator: "and"}

But Pydantic model expected Condition with wrapped expression:
  {expression: {operands: [...], operator: "and"}}

Result: Pydantic deserialized it as Condition with expression=None
→ Logic-Nodes failed with "'NoneType' object has no attribute 'operator'"

Fix:
1. Changed WorkflowNode.condition type from Condition to Any
2. Executor now handles both dict and Pydantic model formats
3. Detects UI format (operator+operands) vs legacy format (expression wrapper)
4. Converts dict to LogicExpression before evaluation

Fixes: Logic-Node execution failures in Training-Tiefenanalyse workflow

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Lars 2026-04-13 08:40:43 +02:00
parent 0eac40abf6
commit 2deb6510f8
2 changed files with 26 additions and 9 deletions

View File

@ -410,17 +410,33 @@ def execute_logic_node(
)
# Handle both serialization formats:
# UI format: condition = {operands: [...], operator: "and"}
# Expected format: condition = {expression: {operands: [...], operator: "and"}}
# UI format: condition = {operands: [...], operator: "and"} (dict or LogicExpression)
# Legacy format: condition = {expression: {operands: [...], operator: "and"}} (Condition object)
expression = None
if hasattr(node.condition, 'operator') and hasattr(node.condition, 'operands'):
# UI format (direct) - check that operator is not None
if node.condition.operator is not None:
# Convert to dict if it's a Pydantic model
condition_dict = node.condition
if hasattr(node.condition, 'model_dump'):
condition_dict = node.condition.model_dump()
elif hasattr(node.condition, 'dict'):
condition_dict = node.condition.dict()
# Check if it's a dict
if isinstance(condition_dict, dict):
# UI format: direct LogicExpression
if 'operator' in condition_dict and 'operands' in condition_dict:
from workflow_models import LogicExpression
expression = LogicExpression(**condition_dict)
# Legacy format: wrapped in Condition
elif 'expression' in condition_dict and condition_dict['expression'] is not None:
from workflow_models import LogicExpression
expression = LogicExpression(**condition_dict['expression'])
# Pydantic object
else:
if hasattr(node.condition, 'operator') and hasattr(node.condition, 'operands'):
expression = node.condition
elif hasattr(node.condition, 'expression') and node.condition.expression is not None:
# Expected format (wrapped) - check that expression is not None
expression = node.condition.expression
elif hasattr(node.condition, 'expression') and node.condition.expression is not None:
expression = node.condition.expression
if expression is None:
error_msg = f"Logic node {node.id} has invalid or empty condition (operator/operands/expression is None or missing)"

View File

@ -195,7 +195,8 @@ class WorkflowNode(BaseModel):
question_augmentations: Optional[List[QuestionAugmentation]] = Field(None, description="Fragenergänzungen (knotengebunden, überschreiben Prompt-Defaults)")
# LOGIC-Knoten
condition: Optional[Condition] = Field(None, description="Bedingung für Pfad-Routing")
# Support both formats: direct LogicExpression (UI) or wrapped in Condition (legacy)
condition: Optional[Any] = Field(None, description="Bedingung für Pfad-Routing (LogicExpression or Condition)")
fallback: Optional[FallbackConfig] = Field(None, description="Fallback-Konfiguration")
# JOIN-Knoten (Phase 4)