refactor: Proper type-safe condition handling with Union types
All checks were successful
Deploy Development / deploy (push) Successful in 52s
Build Test / pytest-backend (push) Successful in 5s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 16s

Previous fix used Any type, breaking type safety and only handling
simple cases. This is the correct implementation:

Changes:
1. LogicExpression.operands: List[Any] → List['LogicExpression']
   - Enables recursive/nested expressions
   - Proper type checking for all operator combinations

2. WorkflowNode.condition: Any → Union[LogicExpression, Condition]
   - Type-safe deserialization
   - Supports both UI format (direct LogicExpression) and legacy (Condition wrapper)
   - Pydantic automatically tries LogicExpression first, then Condition

3. Executor: Simplified with isinstance() checks
   - Clean type detection without dict manipulation
   - Fallback for edge cases

This now correctly handles:
- Simple conditions: {operator: "eq", ref: "...", value: "..."}
- Combined: {operator: "and", operands: [...]}
- Nested: {operator: "or", operands: [{operator: "and", ...}, ...]}
- All operators: eq, neq, in, not_in, gt, lt, gte, lte, contains, and, or, not
- Legacy format: {expression: {...}, then_path: "...", else_path: "..."}

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

View File

@ -409,37 +409,28 @@ def execute_logic_node(
completed_at=datetime.utcnow().isoformat() completed_at=datetime.utcnow().isoformat()
) )
# Handle both serialization formats: # Handle both formats (thanks to Union[LogicExpression, Condition] type):
# UI format: condition = {operands: [...], operator: "and"} (dict or LogicExpression) # 1. Direct LogicExpression (UI format): node.condition is LogicExpression
# Legacy format: condition = {expression: {operands: [...], operator: "and"}} (Condition object) # 2. Wrapped in Condition (legacy): node.condition is Condition with .expression
from workflow_models import LogicExpression, Condition
expression = None expression = None
# Convert to dict if it's a Pydantic model if isinstance(node.condition, LogicExpression):
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 # UI format: direct LogicExpression
if 'operator' in condition_dict and 'operands' in condition_dict: expression = node.condition
from workflow_models import LogicExpression elif isinstance(node.condition, Condition):
expression = LogicExpression(**condition_dict)
# Legacy format: wrapped in Condition # Legacy format: wrapped in Condition
elif 'expression' in condition_dict and condition_dict['expression'] is not None: expression = node.condition.expression
from workflow_models import LogicExpression
expression = LogicExpression(**condition_dict['expression'])
# Pydantic object
else: else:
# Fallback: try to detect format manually
if hasattr(node.condition, 'operator') and hasattr(node.condition, 'operands'): if hasattr(node.condition, 'operator') and hasattr(node.condition, 'operands'):
expression = node.condition expression = node.condition # Looks like LogicExpression
elif hasattr(node.condition, 'expression') and node.condition.expression is not None: elif hasattr(node.condition, 'expression'):
expression = node.condition.expression expression = node.condition.expression # Looks like Condition
if expression is None: if expression is None:
error_msg = f"Logic node {node.id} has invalid or empty condition (operator/operands/expression is None or missing)" error_msg = f"Logic node {node.id} has no valid condition/expression defined"
logger.error(error_msg) logger.error(error_msg)
return NodeExecutionState( return NodeExecutionState(
node_id=node.id, node_id=node.id,

View File

@ -6,7 +6,7 @@ Data validation schemas for Workflow-Graph, Knoten, Kanten, Bedingungen.
Konzept-Basis: konzept_workflow_engine_konsolidated.md Konzept-Basis: konzept_workflow_engine_konsolidated.md
Anforderungsanalyse: anforderungsanalyse_umsetzungsplan.md Anforderungsanalyse: anforderungsanalyse_umsetzungsplan.md
""" """
from typing import Optional, List, Dict, Any from typing import Optional, List, Dict, Any, Union
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from enum import Enum from enum import Enum
@ -148,11 +148,14 @@ class LogicExpression(BaseModel):
} }
""" """
operator: LogicOperator = Field(..., description="Logischer Operator (and, or, not) oder Vergleichsoperator") operator: LogicOperator = Field(..., description="Logischer Operator (and, or, not) oder Vergleichsoperator")
operands: Optional[List[Any]] = Field(None, description="Liste von Operanden (LogicOperand oder verschachtelte LogicExpression)") operands: Optional[List['LogicExpression']] = Field(None, description="Liste von Operanden (LogicOperand oder verschachtelte LogicExpression)")
# Bei einfachem Vergleich: # Bei einfachem Vergleich:
ref: Optional[str] = Field(None, description="Signal-Referenz (nur bei Vergleichsoperatoren)") ref: Optional[str] = Field(None, description="Signal-Referenz (nur bei Vergleichsoperatoren)")
value: Optional[Any] = Field(None, description="Vergleichswert (nur bei Vergleichsoperatoren)") value: Optional[Any] = Field(None, description="Vergleichswert (nur bei Vergleichsoperatoren)")
# Enable forward reference resolution for recursive model
LogicExpression.model_rebuild()
class Condition(BaseModel): class Condition(BaseModel):
""" """
@ -196,7 +199,7 @@ class WorkflowNode(BaseModel):
# LOGIC-Knoten # LOGIC-Knoten
# Support both formats: direct LogicExpression (UI) or wrapped in Condition (legacy) # 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)") condition: Optional[Union[LogicExpression, Condition]] = Field(None, description="Bedingung für Pfad-Routing")
fallback: Optional[FallbackConfig] = Field(None, description="Fallback-Konfiguration") fallback: Optional[FallbackConfig] = Field(None, description="Fallback-Konfiguration")
# JOIN-Knoten (Phase 4) # JOIN-Knoten (Phase 4)