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()
)
# Handle both serialization formats:
# UI format: condition = {operands: [...], operator: "and"} (dict or LogicExpression)
# Legacy format: condition = {expression: {operands: [...], operator: "and"}} (Condition object)
# Handle both formats (thanks to Union[LogicExpression, Condition] type):
# 1. Direct LogicExpression (UI format): node.condition is LogicExpression
# 2. Wrapped in Condition (legacy): node.condition is Condition with .expression
from workflow_models import LogicExpression, Condition
expression = 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):
if isinstance(node.condition, LogicExpression):
# UI format: direct LogicExpression
if 'operator' in condition_dict and 'operands' in condition_dict:
from workflow_models import LogicExpression
expression = LogicExpression(**condition_dict)
expression = node.condition
elif isinstance(node.condition, Condition):
# 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
expression = node.condition.expression
else:
# Fallback: try to detect format manually
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:
expression = node.condition.expression
expression = node.condition # Looks like LogicExpression
elif hasattr(node.condition, 'expression'):
expression = node.condition.expression # Looks like Condition
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)
return NodeExecutionState(
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
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 enum import Enum
@ -148,11 +148,14 @@ class LogicExpression(BaseModel):
}
"""
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:
ref: Optional[str] = Field(None, description="Signal-Referenz (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):
"""
@ -196,7 +199,7 @@ class WorkflowNode(BaseModel):
# LOGIC-Knoten
# 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")
# JOIN-Knoten (Phase 4)